docs(corpus): extract TSC from THEMING into eidos/TSC.md (stub-split)

First piece of the THEMING split, using the renumber-safe stub approach so the
many "THEMING §N" citations across the corpus + code keep resolving.

§7 (Token Scope Contract) and §18 (universal coverage, v2.2) move to a new
src/uix/eidos/TSC.md — the eidos visual canon (E2), with CANON.md-style
frontmatter. THEMING.md keeps numbered pointer-stubs at §7/§18, so section
numbers (and therefore §23/§25/§26/§27/§28 citations) are untouched. 2550 -> 2271
lines; 34 headers intact, TOC anchors still resolve.

Remaining split pieces (own commits): guides §8/§9 -> E4, comparison §15 +
FAQ §17 -> E3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent ffcd1051fd
commit 6ba268065d

@ -42,7 +42,10 @@ permanente/efímero, drift, dos idiomas.
### Fase 4 — partir THEMING (checklists + índice ya hechos, ver HECHO)
- **Partir `THEMING.md`** (2571 L, **34 secciones**, multi-estrato): TSC (§7, §18) → canon visual E2; "añadir componente" (§8)/"definir theme" (§9) → E4 guía; comparación (§15)/FAQ (§17) → E3. El resto (§1-6 mental model/capas/roles/sizes/naming, §10-13 runtime/bundle/validación/sema) = E1 referencia de capa.
- **YA hecho** (commit de §14): §14 motion saneado — tenía una **contradicción** con `eidos-motion.md` (THEMING decía "motion deferred / data-motion-ref no existe / superseded por TSC event:*"; eidos-motion.md dice F1-F7 implementado + motor en `arts/motion` + el event:* scope es el obsoleto). Reescrito como puntero a `eidos-motion.md` (canónico) + conservado el token theming `--motion-scale-lift`.
- **RIESGO del split completo** (= el del rename): las secciones están citadas por `§N` a través del corpus (`CLAUDE.md` §23/25/26/27/28, RFCs §25, `arts/color/README` §26, `THEMING_AUDIT` §23) + las §25/29/30/31 solapan los RFCs de color/depth/shape/structure (ya indexados en `docs/decisions.md`). Partir y renumerar rompe esas citas → necesita sweep. Además §20-34 son añadidos datados tipo changelog. **Pase dedicado con plan + decisión del usuario sobre el sweep antes de carvear.**
- **RIESGO** (= el del rename): las secciones están citadas por `§N` a través del corpus (`CLAUDE.md` §23/25/26/27/28, RFCs §25, `arts/color/README` §26, `THEMING_AUDIT` §23) + las §25/29/30/31 solapan los RFCs de color/depth/shape/structure (ya en `docs/decisions.md`). Renumerar rompe esas citas.
- **Enfoque elegido (usuario): split con stubs** — renumber-safe, sin sweep. Extraer cada bloque a su estrato y dejar un stub-puntero numerado en THEMING manteniendo el número de sección → las citas `§N` sobreviven.
- **HECHO — TSC**: §7 (Token Scope Contract) + §18 (cobertura universal v2.2) → `src/uix/eidos/TSC.md` (canon visual E2, frontmatter como CANON.md). En THEMING quedan stubs §7/§18 apuntando a TSC.md. 2550→2271 L. Verificado: 34 headers intactos, §8/§19 limpios, anchors del TOC OK, §23/25/26/27/28 sin tocar.
- **PENDIENTE del split**: guías §8 (añadir componente) + §9 (definir theme) → E4; comparación §15 + FAQ §17 → E3. Mismo patrón stub. §20-34 (datados, changelog) quedan in-place (citados). NOTA: escribir THEMING con PowerShell `Set-Content`/regex lo bloquea un analizador del harness (lo lee como `Remove-Item`); usar `[System.IO.File]::WriteAllLines` + `.StartsWith()` (sin regex ni ` / ` sueltos), o las tools Edit/Write.
### Fase 5 — huecos de libro (escribir nuevo)
Glosario del vocabulario inventado · arco *getting-started* · decision-log consolidado (semilla: `src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md`) · walkthrough "construye tu propia capa" · comparativa honesta vs Radix/Ark/Mantine (hoy dispersa en THEMING §15, eidos-motion §16) · historia transversal SSR/testing/codegen.

@ -672,267 +672,13 @@ Y corres `npm run generate:eidos-css`.
## 7. Token Scope Contract (TSC)
> Eidos does not infer token scope from emitted CSS. Token scope is
> part of the source contract. The generator emits CSS from scoped
> declarations and validates that every token dependency is available
> in the consumer scope.
TSC es la pieza arquitectónica que distingue a Eidos de Tailwind /
Radix / Chakra / Mantine / shadcn. Resuelve un problema sutil pero
crítico que ningún otro sistema cierra estructuralmente.
### El problema que resuelve
CSS custom property substitution es **eager**, no lazy:
```css
:root {
--base: black;
--derived: var(--base);
}
.x { --base: red; }
.y { background: var(--derived); }
```
¿Qué color tiene `.x.y`? **NEGRO**, no rojo. `--derived` se computa
en `:root` con `--base=black` y se hereda como `black`. El override
de `.x` sobre `--base` no afecta a `--derived` ya congelado.
Aplicado al Toggle pre-TSC:
```css
:root {
--toggle-palette-solid: var(--toggle-color-neutral-solid);
--toggle-solid-on-bg: var(--toggle-palette-solid); /* CONGELADO */
}
[data-toggle][data-color='affirm'] {
--toggle-palette-solid: var(--toggle-color-affirm-solid); /* INÚTIL */
}
```
`--toggle-solid-on-bg` quedaba congelado al neutral. El toggle con
`data-color='affirm'` mostraba gris en vez de teal. **Bug
arquitectónico** que ningún linter detectaría.
### Cómo TSC lo cierra
El config del recipe declara **dónde** se emite cada token:
```ts
recipes.toggle = {
'palette-solid': {
declarations: [
{ value: 'var(--toggle-color-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-color-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-color-threat-solid)', scope: 'color:threat' }
]
},
'solid-on-bg': {
value: 'var(--toggle-palette-solid)',
scope: 'host' // ← obligatorio: dep está en 'host', no en 'root'
}
}
```
El generador:
1. **Infiere `depends`** parseando `var(--{c}-XXX)` del value.
2. **Valida transitivamente**: `solid-on-bg` (scope `host`) depende
de `palette-solid` (scope `host` o más específico) — OK.
3. **Emite cada declaración bajo su selector**: `host` → `[data-toggle]`,
`color:affirm` → `[data-toggle][data-color='affirm']`, etc.
4. **Falla el build** si el scope del consumer no cubre el del dep.
### Scopes disponibles
| Scope | Selector generado | Cuándo usar |
|---|---|---|
| `'root'` | `:root` | Token estable. Default para bare-string. |
| `'host'` | `[data-{c}]` | Token referencia `var(--{c}-palette-*)` u otro `host` token. |
| `color:${v}` | `[data-{c}][data-color='${v}']` | Override del palette por color value. |
| `variant:${v}` | `[data-{c}][data-variant='${v}']` | Cascada de variante. |
| `state:${v}` | `[data-{c}][data-state='${v}']` | Cascada de estado. |
| `size:${v}` | `[data-{c}][data-size='${v}']` | Cascada de tamaño. |
| `event:${v}` | `[data-{c}][data-event='${v}']` | Token de motion ligado a señal perceptual. |
| `[axis:v, …]` | `[data-{c}][data-X='v'][data-Y='w']` | **Composite** — múltiples condiciones ANDed. |
### Tres formas de declarar un token
```ts
recipes.toggle = {
// (1) Forma corta — scope 'root' implícito (token estable)
'height-md': '32px',
// (2) Forma simple — una declaración con scope explícito
// depends se infiere automáticamente de var() en el value
'solid-on-bg': {
value: 'var(--toggle-palette-solid)',
scope: 'host'
},
// (3) Forma multi-declaración — el MISMO token bajo distintos scopes
// (la realidad CSS de un custom property redeclarado por cascada)
'palette-solid': {
declarations: [
{ value: 'var(--toggle-color-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-color-affirm-solid)', scope: 'color:affirm' }
]
}
};
```
### Álgebra de scope (`scopeCovers`)
No es un orden total simple. La regla es:
> `consumer scopeCovers dep` ⇔ todo elemento que matchea el consumer's
> scope también matchea el dep's scope.
Equivalentemente: las constraints del dep deben ser un **subconjunto**
de las constraints del consumer.
| consumer | dep | covers? | Razón |
|---|---|---|---|
| `host` | `root` | ✓ | host es más específico, root siempre aplica |
| `host` | `host` | ✓ | mismo scope |
| `host` | `color:affirm` | ✗ | consumer no constraint el color |
| `color:affirm` | `host` | ✓ | host cubre todo el host scope |
| `color:affirm` | `color:affirm` | ✓ | mismo scope |
| `color:affirm` | `color:loss` | ✗ | scopes incompatibles (diferentes values del mismo axis) |
| `color:affirm` | `size:lg` | ✗ | consumer no constraint el size |
| `[color:affirm, size:lg]` | `color:affirm` | ✓ | composite cubre cada componente |
| `[color:affirm, size:lg]` | `size:lg` | ✓ | igual |
### Cross-axis collision detection
Si un token tiene declaraciones en axes incomparables (e.g.
`color:affirm` y `state:on`), un elemento con ambos atributos matchea
ambos bloques. El cascade winner depende de orden de declaración —
silent correctness bug.
El generador detecta esto y **exige una declaración composite** que
desambigüe:
```ts
'bg': {
declarations: [
{ value: 'red', scope: 'color:affirm' },
{ value: 'blue', scope: 'state:on' },
{ value: 'purple', scope: ['color:affirm', 'state:on'] } // ← obligatorio
]
}
```
Sin la composite, build falla:
```
Eidos recipe scope contract violations:
- synth.bg: declarations at scopes color:affirm and state:on can both
apply to the same element. Add an explicit composite declaration
[color:affirm, state:on] to disambiguate cascade order.
```
### Multi-part scope — `parts: [...]` (TSC v2.2)
Cuando `data-color` (u otro axis TSC) NO vive en el root del componente
sino en parts específicos, el generador emite una regla con selector
comma-separado:
```ts
// recipes.select._accent-track
{
parts: ['trigger', 'content'],
declarations: [
{ value: 'var(--select-primary-track)', scope: 'host' },
{ value: 'var(--select-affirm-track)', scope: 'color:affirm' }
]
}
```
Genera:
```css
[data-select-trigger], [data-select-content] {
--_select-accent-track: var(--select-primary-track);
}
[data-select-trigger][data-color='affirm'], [data-select-content][data-color='affirm'] {
--_select-accent-track: var(--select-affirm-track);
}
```
**Cuándo usarlo**: el componente porta `data-color` per-part (típicamente
porque un part viaja por portal y se renderiza fuera del árbol DOM del
otro). Single-part components siguen sin necesitar `parts` — el default
`[data-{c}]` es lo correcto.
**Quién lo usa hoy**: `select` (trigger + content) — único caso real
en el catálogo. Los demás componentes con `data-color` lo declaran en
el root.
### Cross-recipe composition — `composition: { ... }` (TSC v2.2)
Cuando un recipe necesita modificar tokens de OTRO recipe scoped a su
propio cascade, declara un bloque `composition` sibling de los tokens
regulares:
```ts
// recipes.toggle-group
{
gap: 'var(--space-1)',
composition: {
toggle: { // foreign recipe name
targetSelector: '[data-toggle-group-item]', // descendant selector
tokens: {
'palette-solid': {
declarations: [
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-solid)', scope: 'color:risk' }
]
}
}
}
}
}
```
Genera:
```css
[data-toggle-group][data-color='affirm'] [data-toggle-group-item] {
--toggle-palette-solid: var(--toggle-affirm-solid);
}
[data-toggle-group][data-color='risk'] [data-toggle-group-item] {
--toggle-palette-solid: var(--toggle-risk-solid);
}
```
Reglas:
- El CSS variable name se deriva del recipe FORÁNEO
(`--toggle-palette-solid`), no del host. Para tokens privados del
foreign use `_palette-solid` → `--_toggle-palette-solid`.
- El selector es `{host's scope-rule} {targetSelector}` — combinación
ancestor + descendant.
- Las composition declarations DEBEN tener scope ≠ `'root'`. Un
override no-scoped pertenece al foreign recipe, no al composition
block. El validador rechaza root-scoped composition entries.
- Composition NO se valida con el algebra de scope del host (las
composition entries modifican TOKENS del foreign, no del host), pero
sí pasa por el mismo pipeline de validación general
(`validateRecipeComposition`).
**Quién lo usa hoy**: `toggle-group` (modifica `--toggle-palette-*` en
sus items). Pattern reutilizable para futuros wrappers compositivos
(button-group, nav-menu).
### Pipeline de defensas (5 capas)
```
1. tsc --noEmit ← TS bien tipado
2. TSC scope algebra ← ningún token depende de scope más dinámico
3. TSC cross-axis check ← composites obligatorios donde hay collision
4. eidos-lint ← defensa secundaria del CSS generado
5. runtime probe ← confirma comportamiento real en browser
```
> **Movido a [`TSC.md`](./TSC.md)** (canon visual, E2). El Token Scope Contract
> —scopes disponibles, las tres formas de declarar un token, el álgebra de
> `scopeCovers`, la detección de colisión cross-axis, multi-part scope y
> cross-recipe composition (v2.2), y el pipeline de 5 defensas— vive ahí como su
> propio capítulo. Resumen: el TSC decide DÓNDE se emite cada token (raíz, por
> componente, por color, por evento) y valida al generar que toda dependencia
> esté disponible en el scope del consumidor.
---
@ -1676,34 +1422,9 @@ Pendientes deferred:
## 18. Cobertura universal de TSC
**Los 15 componentes con `data-color` están en TSC**. No hay
excepciones arquitectónicas — TSC v2.2 cubre las 3 patrones que
antes vivían fuera del modelo:
| Patrón | Solución TSC v2.2 | Componentes |
|---|---|---|
| `data-color` per-parte (no en root) | `parts: ['x', 'y']` en `RecipeTokenMultiDeclaration` (multi-part scope) | `select` (trigger + content) |
| Composite (variant × color) | `scope: ['variant:X', 'color:Y']` (TSC v2 composite) | `avatar` (root + badge) |
| Cross-recipe override desde ancestor | `composition: { foreignRecipe: { targetSelector, tokens } }` | `toggle-group` (modifica Toggle's palette) |
El guard universal `forbids palette-derived tokens at :root scope`
(en `recipe-css-contract.test.ts`) sigue activo como defensa
secundaria en el CSS final, pero la fuente de verdad es el contrato
de tipos.
### 18.1 Cuándo se añadió cada extensión
- **Multi-part scope** (TSC v2.2): permite que un token cascadee sobre
más de un selector raíz. Necesario cuando `data-color` vive en parts
distintos por razones de portal/cascade (Select Content vive fuera
del árbol DOM del Trigger).
- **Composition** (TSC v2.2): permite que un recipe declare overrides
de los tokens de OTRO recipe, scoped a sus propias condiciones.
Necesario para wrappers compositivos (toggle-group, eventual
button-group, nav-menu, etc.).
Ambas extensiones se validan con el mismo pipeline TSC (scope
algebra + cross-axis collision detection + auto-inferred deps).
> **Movido a [`TSC.md`](./TSC.md)**. Las extensiones v2.2 (multi-part scope y
> cross-recipe composition) que llevan el TSC a cobertura universal viven con el
> resto del contrato en `TSC.md`.
---

@ -0,0 +1,315 @@
---
title: Token Scope Contract (TSC)
type: canon
audience: human + agent
authority: canonical — where every eidos token is allowed to be emitted
status: current
source: extracted from src/uix/eidos/THEMING.md (was §7 + §18)
---
# Token Scope Contract (TSC)
> El TSC decide DÓNDE se emite cada token de eidos (`:root` / `[data-{c}]` /
> `[data-{c}][data-color=...]` / ...) y valida la transitividad al generar. Es
> el contrato visual canónico (E2) que [`THEMING.md`](./THEMING.md) §7 referencia.
> Se extrajo de THEMING para vivir como su propio capítulo; la cobertura
> universal v2.2 (antes THEMING §18) se incluye al final.
---
> Eidos does not infer token scope from emitted CSS. Token scope is
> part of the source contract. The generator emits CSS from scoped
> declarations and validates that every token dependency is available
> in the consumer scope.
TSC es la pieza arquitectónica que distingue a Eidos de Tailwind /
Radix / Chakra / Mantine / shadcn. Resuelve un problema sutil pero
crítico que ningún otro sistema cierra estructuralmente.
### El problema que resuelve
CSS custom property substitution es **eager**, no lazy:
```css
:root {
--base: black;
--derived: var(--base);
}
.x { --base: red; }
.y { background: var(--derived); }
```
¿Qué color tiene `.x.y`? **NEGRO**, no rojo. `--derived` se computa
en `:root` con `--base=black` y se hereda como `black`. El override
de `.x` sobre `--base` no afecta a `--derived` ya congelado.
Aplicado al Toggle pre-TSC:
```css
:root {
--toggle-palette-solid: var(--toggle-color-neutral-solid);
--toggle-solid-on-bg: var(--toggle-palette-solid); /* CONGELADO */
}
[data-toggle][data-color='affirm'] {
--toggle-palette-solid: var(--toggle-color-affirm-solid); /* INÚTIL */
}
```
`--toggle-solid-on-bg` quedaba congelado al neutral. El toggle con
`data-color='affirm'` mostraba gris en vez de teal. **Bug
arquitectónico** que ningún linter detectaría.
### Cómo TSC lo cierra
El config del recipe declara **dónde** se emite cada token:
```ts
recipes.toggle = {
'palette-solid': {
declarations: [
{ value: 'var(--toggle-color-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-color-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-color-threat-solid)', scope: 'color:threat' }
]
},
'solid-on-bg': {
value: 'var(--toggle-palette-solid)',
scope: 'host' // ← obligatorio: dep está en 'host', no en 'root'
}
}
```
El generador:
1. **Infiere `depends`** parseando `var(--{c}-XXX)` del value.
2. **Valida transitivamente**: `solid-on-bg` (scope `host`) depende
de `palette-solid` (scope `host` o más específico) — OK.
3. **Emite cada declaración bajo su selector**: `host` → `[data-toggle]`,
`color:affirm` → `[data-toggle][data-color='affirm']`, etc.
4. **Falla el build** si el scope del consumer no cubre el del dep.
### Scopes disponibles
| Scope | Selector generado | Cuándo usar |
|---|---|---|
| `'root'` | `:root` | Token estable. Default para bare-string. |
| `'host'` | `[data-{c}]` | Token referencia `var(--{c}-palette-*)` u otro `host` token. |
| `color:${v}` | `[data-{c}][data-color='${v}']` | Override del palette por color value. |
| `variant:${v}` | `[data-{c}][data-variant='${v}']` | Cascada de variante. |
| `state:${v}` | `[data-{c}][data-state='${v}']` | Cascada de estado. |
| `size:${v}` | `[data-{c}][data-size='${v}']` | Cascada de tamaño. |
| `event:${v}` | `[data-{c}][data-event='${v}']` | Token de motion ligado a señal perceptual. |
| `[axis:v, …]` | `[data-{c}][data-X='v'][data-Y='w']` | **Composite** — múltiples condiciones ANDed. |
### Tres formas de declarar un token
```ts
recipes.toggle = {
// (1) Forma corta — scope 'root' implícito (token estable)
'height-md': '32px',
// (2) Forma simple — una declaración con scope explícito
// depends se infiere automáticamente de var() en el value
'solid-on-bg': {
value: 'var(--toggle-palette-solid)',
scope: 'host'
},
// (3) Forma multi-declaración — el MISMO token bajo distintos scopes
// (la realidad CSS de un custom property redeclarado por cascada)
'palette-solid': {
declarations: [
{ value: 'var(--toggle-color-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-color-affirm-solid)', scope: 'color:affirm' }
]
}
};
```
### Álgebra de scope (`scopeCovers`)
No es un orden total simple. La regla es:
> `consumer scopeCovers dep` ⇔ todo elemento que matchea el consumer's
> scope también matchea el dep's scope.
Equivalentemente: las constraints del dep deben ser un **subconjunto**
de las constraints del consumer.
| consumer | dep | covers? | Razón |
|---|---|---|---|
| `host` | `root` | ✓ | host es más específico, root siempre aplica |
| `host` | `host` | ✓ | mismo scope |
| `host` | `color:affirm` | ✗ | consumer no constraint el color |
| `color:affirm` | `host` | ✓ | host cubre todo el host scope |
| `color:affirm` | `color:affirm` | ✓ | mismo scope |
| `color:affirm` | `color:loss` | ✗ | scopes incompatibles (diferentes values del mismo axis) |
| `color:affirm` | `size:lg` | ✗ | consumer no constraint el size |
| `[color:affirm, size:lg]` | `color:affirm` | ✓ | composite cubre cada componente |
| `[color:affirm, size:lg]` | `size:lg` | ✓ | igual |
### Cross-axis collision detection
Si un token tiene declaraciones en axes incomparables (e.g.
`color:affirm` y `state:on`), un elemento con ambos atributos matchea
ambos bloques. El cascade winner depende de orden de declaración —
silent correctness bug.
El generador detecta esto y **exige una declaración composite** que
desambigüe:
```ts
'bg': {
declarations: [
{ value: 'red', scope: 'color:affirm' },
{ value: 'blue', scope: 'state:on' },
{ value: 'purple', scope: ['color:affirm', 'state:on'] } // ← obligatorio
]
}
```
Sin la composite, build falla:
```
Eidos recipe scope contract violations:
- synth.bg: declarations at scopes color:affirm and state:on can both
apply to the same element. Add an explicit composite declaration
[color:affirm, state:on] to disambiguate cascade order.
```
### Multi-part scope — `parts: [...]` (TSC v2.2)
Cuando `data-color` (u otro axis TSC) NO vive en el root del componente
sino en parts específicos, el generador emite una regla con selector
comma-separado:
```ts
// recipes.select._accent-track
{
parts: ['trigger', 'content'],
declarations: [
{ value: 'var(--select-primary-track)', scope: 'host' },
{ value: 'var(--select-affirm-track)', scope: 'color:affirm' }
]
}
```
Genera:
```css
[data-select-trigger], [data-select-content] {
--_select-accent-track: var(--select-primary-track);
}
[data-select-trigger][data-color='affirm'], [data-select-content][data-color='affirm'] {
--_select-accent-track: var(--select-affirm-track);
}
```
**Cuándo usarlo**: el componente porta `data-color` per-part (típicamente
porque un part viaja por portal y se renderiza fuera del árbol DOM del
otro). Single-part components siguen sin necesitar `parts` — el default
`[data-{c}]` es lo correcto.
**Quién lo usa hoy**: `select` (trigger + content) — único caso real
en el catálogo. Los demás componentes con `data-color` lo declaran en
el root.
### Cross-recipe composition — `composition: { ... }` (TSC v2.2)
Cuando un recipe necesita modificar tokens de OTRO recipe scoped a su
propio cascade, declara un bloque `composition` sibling de los tokens
regulares:
```ts
// recipes.toggle-group
{
gap: 'var(--space-1)',
composition: {
toggle: { // foreign recipe name
targetSelector: '[data-toggle-group-item]', // descendant selector
tokens: {
'palette-solid': {
declarations: [
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-solid)', scope: 'color:risk' }
]
}
}
}
}
}
```
Genera:
```css
[data-toggle-group][data-color='affirm'] [data-toggle-group-item] {
--toggle-palette-solid: var(--toggle-affirm-solid);
}
[data-toggle-group][data-color='risk'] [data-toggle-group-item] {
--toggle-palette-solid: var(--toggle-risk-solid);
}
```
Reglas:
- El CSS variable name se deriva del recipe FORÁNEO
(`--toggle-palette-solid`), no del host. Para tokens privados del
foreign use `_palette-solid` → `--_toggle-palette-solid`.
- El selector es `{host's scope-rule} {targetSelector}` — combinación
ancestor + descendant.
- Las composition declarations DEBEN tener scope ≠ `'root'`. Un
override no-scoped pertenece al foreign recipe, no al composition
block. El validador rechaza root-scoped composition entries.
- Composition NO se valida con el algebra de scope del host (las
composition entries modifican TOKENS del foreign, no del host), pero
sí pasa por el mismo pipeline de validación general
(`validateRecipeComposition`).
**Quién lo usa hoy**: `toggle-group` (modifica `--toggle-palette-*` en
sus items). Pattern reutilizable para futuros wrappers compositivos
(button-group, nav-menu).
### Pipeline de defensas (5 capas)
```
1. tsc --noEmit ← TS bien tipado
2. TSC scope algebra ← ningún token depende de scope más dinámico
3. TSC cross-axis check ← composites obligatorios donde hay collision
4. eidos-lint ← defensa secundaria del CSS generado
5. runtime probe ← confirma comportamiento real en browser
```
---
## Cobertura universal de TSC (v2.2)
**Los 15 componentes con `data-color` están en TSC**. No hay
excepciones arquitectónicas — TSC v2.2 cubre las 3 patrones que
antes vivían fuera del modelo:
| Patrón | Solución TSC v2.2 | Componentes |
|---|---|---|
| `data-color` per-parte (no en root) | `parts: ['x', 'y']` en `RecipeTokenMultiDeclaration` (multi-part scope) | `select` (trigger + content) |
| Composite (variant × color) | `scope: ['variant:X', 'color:Y']` (TSC v2 composite) | `avatar` (root + badge) |
| Cross-recipe override desde ancestor | `composition: { foreignRecipe: { targetSelector, tokens } }` | `toggle-group` (modifica Toggle's palette) |
El guard universal `forbids palette-derived tokens at :root scope`
(en `recipe-css-contract.test.ts`) sigue activo como defensa
secundaria en el CSS final, pero la fuente de verdad es el contrato
de tipos.
### Cuándo se añadió cada extensión
- **Multi-part scope** (TSC v2.2): permite que un token cascadee sobre
más de un selector raíz. Necesario cuando `data-color` vive en parts
distintos por razones de portal/cascade (Select Content vive fuera
del árbol DOM del Trigger).
- **Composition** (TSC v2.2): permite que un recipe declare overrides
de los tokens de OTRO recipe, scoped a sus propias condiciones.
Necesario para wrappers compositivos (toggle-group, eventual
button-group, nav-menu, etc.).
Ambas extensiones se validan con el mismo pipeline TSC (scope
algebra + cross-axis collision detection + auto-inferred deps).
---
Loading…
Cancel
Save

Powered by TurnKey Linux.