Barrido de lo que la sesion cambio y la documentacion todavia no decia.
`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.
`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.
`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.
`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.
`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.
`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.
docs:check 0/627.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| state-layer (`var(--state-*)`) | 20/130 | El resto lo hereda de `archetypes.css` vía arquetipo; el único hover fuera de contrato que queda es badge (R-4.3, `color-mix(currentColor)`), confirmado por la matriz de aceptación. |
| state-layer (`var(--state-*)`) | 20/130 | El resto lo hereda de `archetypes.css` vía arquetipo; el único hover fuera de contrato que queda es badge (R-4.3, `color-mix(currentColor)`), confirmado por la matriz de aceptación. |
| depth/shadow tokens | 10/130 | Coherente con el modelo `data-depth` (el bundle lo pinta la fundación); no hay sombras literales. |
| depth/shadow tokens | 10/130 | Coherente con el modelo `data-depth` (el bundle lo pinta la fundación); no hay sombras literales. |
| focus propio | 73/130 | El resto cae al anillo de fundación (ver EID-1). 7 warns R-1.5 pendientes en la matriz. |
| focus propio | 73/130 | El resto cae al anillo de fundación (ver EID-1). 7 warns R-1.5 pendientes en la matriz. |
| `data-size` en CSS | 78/130 | Consistente con los subsets declarados; 10 fallos de paridad de chips (D-7.4) son de demo, no de receta. |
| `data-size` en CSS | 78/130 | Consistente con los subsets declarados; 10 fallos de paridad de chips (D-7.4) son de demo, no de receta. |
| motion tokens propios | 37/130 | El resto usa presets/signatures generados — el diseño esperado. |
| motion tokens propios | 37/130 | El resto usa presets/signatures generados — el diseño esperado. |
| props físicas left/right | 5/130 | Todas son APIs de colocación (EID-3). |
| props físicas left/right | 5/130 | Todas son APIs de colocación (EID-3). |
Estado por reglas de la matriz de aceptación (fresco, 2026-07-10):
Estado por reglas de la matriz de aceptación (fresco, 2026-07-10):
**R-4.x prácticamente limpio** (1 sola violación viva: badge R-4.3) y
**R-4.x prácticamente limpio** (1 sola violación viva: badge R-4.3) y
| Color (escalas/roles/slots/alphas) | ✅ completo + runtime + CSS-only themes | **THM-2**: paleta 33 por instancia solo en 2/17 componentes. Layer 4 (`--{c}-{role}-{slot}`) sigue sin consumidores — la propia FAQ la marca candidata a colapso "si en 6 meses nadie la usa": toca decidir. |
| Color (escalas/roles/slots/alphas) | ✅ completo + runtime + CSS-only themes | **THM-2**: paleta 33 por instancia solo en 2/17 componentes. Layer 4 (`--{c}-{role}-{slot}`) sigue sin consumidores — la propia FAQ la marca candidata a colapso "si en 6 meses nadie la usa": toca decidir. |
| Tipografía | ✅ fluid + tracking/leading/features/measure + fonts config | Motor de fuentes variables listo pero **assets aún TTF estáticos** (capacidad no ejercida, rfc-typography §5); `--size-{k}-font-letter-spacing` emitido y sin consumir por recetas de control (pendiente declarado); divergencia de 2 px en label `lg` segmentado (pendiente documentado en §5). |
| Tipografía | ✅ fluid + tracking/leading/features/measure + fonts config | Motor de fuentes variables listo pero **assets aún TTF estáticos** (capacidad no ejercida, rfc-typography §5); `--size-{k}-font-letter-spacing` emitido y sin consumir por recetas de control (pendiente declarado); divergencia de 2 px en label `lg` segmentado (pendiente documentado en §5). |
| Espaciado / densidad / scaling | ✅ 3 ejes compuestos + applySpacing | Densidad = 3 niveles fijos (retunables en magnitud, no ampliables en número); cambiar la participación de scaling exige config+regeneración (no runtime) — ambas son decisiones de diseño declaradas, no bugs. |
| Espaciado / densidad / scaling | ✅ 3 ejes compuestos + applySpacing | Densidad = 3 niveles fijos (retunables en magnitud, no ampliables en número); cambiar la participación de scaling exige config+regeneración (no runtime) — ambas son decisiones de diseño declaradas, no bugs. |
| Forma | ✅ smoothing/familias/nesting + applyShape | Nesting a `full` solo esquinas superiores (límite geométrico documentado). |
| Forma | ✅ smoothing/familias/nesting + applyShape | Nesting a `full` solo esquinas superiores (límite geométrico documentado). |
| Profundidad | ✅ planes config-driven + frost + applyDepth | Token `scrim` emitido **sin regla** que lo consuma (backdrop sigue per-component); `--depth-{plane}-z` expuesto con 0 consumidores (open cage declarado — vigilar que no fosilice). |
| Profundidad | ✅ planes config-driven + frost + applyDepth | Token `scrim` emitido **sin regla** que lo consuma (backdrop sigue per-component); `--depth-{plane}-z` expuesto con 0 consumidores (open cage declarado — vigilar que no fosilice). |
| Motion | ✅ dos momentos + presets tipados extensibles + loops + reduce | Presets JS no serializables (registro directo, documentado). Sin carencia real. |
| Motion | ✅ dos momentos + presets tipados extensibles + loops + reduce | Presets JS no serializables (registro directo, documentado). Sin carencia real. |
| Sonido / háptica | ✅ tunings + packs + preferences reduce/off | **Un tema no puede re-sonorizar sin código**: los packs son TS, no config del tema (coherente con D.7 "samples = recursos", pero significa que la personalización sonora es de app/desarrollador, no de theme distribuible). |
| Sonido / háptica | ✅ tunings + packs + preferences reduce/off | **Un tema no puede re-sonorizar sin código**: los packs son TS, no config del tema (coherente con D.7 "samples = recursos", pero significa que la personalización sonora es de app/desarrollador, no de theme distribuible). |
| Focus ring | ✅ `--focus-ring-*` parametrizado | EID-1 (modelo dividido fundación vs canon §32). |
| Focus ring | ✅ `--focus-ring-*` parametrizado | EID-1 (modelo dividido fundación vs canon §32). |
| State layer | ✅ config `primitives.state` | — |
| State layer | ✅ config `primitives.state`| — |
| Touch target | Constante 44 px | **No temable por diseño declarado** (§37: "hard ergonomic constant"). Coherente; anotar en gap solo como decisión. |
| Touch target | Constante 44 px | **No temable por diseño declarado** (§37: "hard ergonomic constant"). Coherente; anotar en gap solo como decisión. |
| Breakpoints / container | ✅ config vía ActiveDom → tokens + `@container` | Container queries: eje temable con **0 consumidores** ("open cage", tsc.md §container) — mismo riesgo de fosilización que scrim/depth-z. |
| Breakpoints / container | ✅ config vía ActiveDom → tokens + `@container` | Container queries: eje temable con **0 consumidores** ("open cage", tsc.md §container) — mismo riesgo de fosilización que scrim/depth-z. |
| Gradientes | ✅ tokens + angles + applyGradients + capstone | Solo deriva documental: reference §10 y channels.md §5 omiten el eje `gradient` que `ThemeSeed` ya compone (`active-eidos.svelte.ts:219-232`). |
| Gradientes | ✅ tokens + angles + applyGradients + capstone | Solo deriva documental: reference §10 y channels.md §5 omiten el eje `gradient` que `ThemeSeed` ya compone (`active-eidos.svelte.ts:219-232`). |
| Iconografía | ✅ `--icon-size-*`/stroke + glifos spin-field temables | No hay noción de "icon set" temable por config (el tema no puede sustituir el set de iconos; decisión razonable, registrar como límite). |
| Iconografía | ✅ `--icon-size-*`/stroke + glifos spin-field temables | No hay noción de "icon set" temable por config (el tema no puede sustituir el set de iconos; decisión razonable, registrar como límite). |
| Scrollbars | Parcial | `scroll-area` tiene receta propia, pero no hay theming sistémico del scrollbar nativo global (colores de `scrollbar-color`/webkit fuera del contrato). |
| Scrollbars | Parcial | `scroll-area` tiene receta propia, pero no hay theming sistémico del scrollbar nativo global (colores de `scrollbar-color`/webkit fuera del contrato). |
| Selección de texto / caret / cursor | ❌ | La fundación no emite `::selection`, `caret-color` ni tokens de cursor; los cursores están cableados por arquetipo (pointer/grab/not-allowed). Si "todos los aspectos" es la vara: son los tres huecos sistémicos visibles. |
| Selección de texto / caret / cursor | ❌ | La fundación no emite `::selection`, `caret-color` ni tokens de cursor; los cursores están cableados por arquetipo (pointer/grab/not-allowed). Si "todos los aspectos" es la vara: son los tres huecos sistémicos visibles. |
| Dark/light/HCM/contraste | ✅ | — |
| Dark/light/HCM/contraste | ✅ | — |
| RTL | ✅ lógico en flujo + `getDirectionalKeys` | EID-3 (placement físico — pendiente de doctrina explícita). |
| RTL | ✅ lógico en flujo + `getDirectionalKeys`| ~~EID-3 (placement físico — pendiente de doctrina explícita)~~ → **cerrado 2026-08-15**: dos rejillas nombradas (`Position` física / `LogicalPosition` lógica) con regla de elección en sus typedoc y en el canon generado. |
| Tema distribuible | ✅ CSS-only vía contrato | **Límite deliberado** (§19): `ThemeDefinition` no expone `recipes` — un tema no puede ajustar tokens per-component (solo el app en boot). Está razonado (portabilidad perceptual); consecuencia práctica: un "marketplace de temas" no podría re-tunear componentes sin pasar por el app. |
| Tema distribuible | ✅ CSS-only vía contrato | **Límite deliberado** (§19): `ThemeDefinition` no expone `recipes` — un tema no puede ajustar tokens per-component (solo el app en boot). Está razonado (portabilidad perceptual); consecuencia práctica: un "marketplace de temas" no podría re-tunear componentes sin pasar por el app. |
---
---
@ -491,34 +497,34 @@ sistemáticas y vale la pena listarlas:
| Control height per size | `control-height-{size}` (or `{part}-height-{size}` when the part is not the control) | inventing a third name per component |
| Control height per size | `control-height-{size}` (or `{part}-height-{size}` when the part is not the control) | inventing a third name per component |
| Padding per axis | `padding-inline[-{size}]` / `padding-block[-{size}]` | **`padding-x` / `padding-y`** AND the abbreviated segments **`px` / `py`** (physical axes) |
| Padding per axis | `padding-inline[-{size}]` / `padding-block[-{size}]` | **`padding-x` / `padding-y`** AND the abbreviated segments **`px` / `py`** (physical axes) |
| **Neutral hover / press / selected** | the state-layer (rules in `archetypes.css`; magnitudes are config data `primitives.state` — changelog §40) | transparent background → `background: var(--state-hover)`; filled background → the overlay `background-image: linear-gradient(var(--state-hover), var(--state-hover))` | R-4.3 |
| **Neutral hover / press / selected** | the state-layer (rules in `archetypes.css`; magnitudes are config data `primitives.state` — changelog §40) | transparent background → `background: var(--state-hover)`; filled background → the overlay `background-image: linear-gradient(var(--state-hover), var(--state-hover))` | R-4.3 |
| **Valenced hover** (solid / soft per color) | the recipe's palette swap | `background: var(--{c}-solid-hover)` etc. — tokens, never hand-rolled `color-mix(currentColor …)` | R-4.3 |
| **Valenced hover** (solid / soft per color) | the recipe's palette swap | `background: var(--{c}-solid-hover)` etc. — tokens, never hand-rolled `color-mix(currentColor …)` | R-4.3 |
| **Elevation** | the scale + the depth channel | `box-shadow: var(--shadow-*)` or compose `var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`; never literal shadows | R-4.1 |
| **Elevation** | the scale + the depth channel | `box-shadow: var(--shadow-*)` or compose `var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`; never literal shadows | R-4.1 |
| **Crisp inner ring** | the inset-ring (THEMING §29) | `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, …)` — the expression lives at the point of use | R-4.1 (passes: it carries `var()`) |
| **Crisp inner ring** | the inset-ring (THEMING §29) | `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, …)` — the expression lives at the point of use | R-4.1 (passes: it carries `var()`) |
| **Focus** | per archetype (THEMING §32) | fields → the two-ring `--focus-ring-*` (box-shadow); surfaces/controls → their own `outline` (HCM-safe); declaring nothing falls back to `archetypes.css` | R-1.5 |
| **Focus** | per archetype (THEMING §32) | fields → the two-ring `--focus-ring-*` (box-shadow); surfaces/controls → their own `outline` (HCM-safe); declaring nothing falls back to `archetypes.css` | R-1.5 |
| **Disabled** | the opacity token | `opacity: var(--opacity-disabled)` + `cursor: not-allowed` — never hand `0.4`/`0.5`/`0.6` | R-4.2 |
| **Disabled** | the opacity token | `opacity: var(--opacity-disabled)` + `cursor: not-allowed` — never hand `0.4`/`0.5`/`0.6` | R-4.2 |
| **Typography** | 1:1 with the scale | see §1; canonical exceptions: avatar/marker (glyph ∝ diameter), accordion (prose scale) | R-2.7 + guard |
| **Typography** | 1:1 with the scale | see §1; canonical exceptions: avatar/marker (glyph ∝ diameter), accordion (prose scale) | R-2.7 + guard |
| **Motion — state transitions** | tokens | `transition: X var(--duration-*) var(--ease-*)` | THEMING §35 block C |
| **Motion — state transitions** | tokens | `transition: X var(--duration-*) var(--ease-*)` | THEMING §35 block C |
| **Motion — event signatures** | `EidosConfig.motion.{keyframes,signatures}` (generated, themeable) | NO local `@keyframes` for perceptual reactions; a local `@keyframes` only when **functional** (spin/shimmer/continuous period) and annotated | R-4.5 |
| **Motion — event signatures** | `EidosConfig.motion.{keyframes,signatures}` (generated, themeable) | NO local `@keyframes` for perceptual reactions; a local `@keyframes` only when **functional** (spin/shimmer/continuous period) and annotated | R-4.5 |
| **Spacing** | `var(--space-*)` | density × scaling arrive composed from the foundation — **never** multiply by `--density-*`/`--scaling` in the recipe | R-2.3 |
| **Spacing** | `var(--space-*)` | density × scaling arrive composed from the foundation — **never** multiply by `--density-*`/`--scaling` in the recipe | R-2.3 |
| **Touch target** | archetype (`trigger`/`close`/`action`) + list-surface | free via `@media (pointer: coarse)` in `archetypes.css` — don't declare your own 44px minimums | — |
| **Touch target** | archetype (`trigger`/`close`/`action`) + list-surface | free via `@media (pointer: coarse)` in `archetypes.css` — don't declare your own 44px minimums | — |
| **List/menu rows** | the list-surface (`--list-item-*`) | list surfaces bridge `--list-item-height`/`-py`, not their own row heights | — |
| **List/menu rows** | the list-surface (`--list-item-*`) | list surfaces bridge `--list-item-height`/`-py`, not their own row heights | — |
| **Floating z-index** | the `--z-index-overlay-*` band | each portaled overlay consumes its rung; raw integers `0..5` only for intra-component order | guard in contracts.test |
| **Floating z-index** | the `--z-index-overlay-*` band | each PORTALED overlay consumes its rung; raw integers `0..5` only for intra-component order | guard in contracts.test |
| **Color** | role slots / recipe tokens | `var(--color-{role}-{slot})` or `var(--{c}-*)`; never direct `--scale-*`/`--primitive-*` nor hex/rgb | R-2.1/2.6, R-4.6 |
| **Viewport-fixed chrome** (not portaled) | the `--z-index-*` ladder, `affix` rung | a consent strip / FAB / floating bar is page chrome, not an overlay: it rides `affix` (150) — above `sticky`, below every menu. Two elements in the SAME band tie, and DOM order breaks the tie (measured: a notice correctly first in the source lost to a sticky header, 30.7px of overlap) | THEME-SYS-1 + `layer:check` |
| **Color** | role slots / recipe tokens | `var(--color-{role}-{slot})` or `var(--{c}-*)`; never direct `--scale-*`/`--primitive-*` nor hex/rgb | R-2.1/2.6, R-4.6 |
## 3. Exceptions — how they are declared
## 3. Exceptions — how they are declared
A deviation is valid **only** when annotated on the same line. The audit
A deviation is valid **only** when annotated on the same line. The audit
honors annotations; an unannotated deviation is drift.
honors annotations; an unannotated deviation is drift.
| `/* literal: <reason> */` | a physically-fixed value or a justified optical tuning | `font-size: 13px; /* literal: tight icon affordance */` |
| `/* literal: <reason> */`| a physically-fixed value or a justified optical tuning | `font-size: 13px; /* literal: tight icon affordance */` |
| `/* functional: <reason> */` | a local `@keyframes` that is NOT a perceptual signature (continuous period, machinery) | `/* functional: continuous spin period, not an event signature */``@keyframes spin { … }` |
| `/* functional: <reason> */` | a local `@keyframes` that is NOT a perceptual signature (continuous period, machinery) | `/* functional: continuous spin period, not an event signature */``@keyframes spin { … }` |
| physically-fixed | colors that must not follow the theme (the QR's white, the natural clock's skies) — hex allowed WITH a comment | `--_qr-bg: #ffffff; /* literal: QR quiet zone must be true white */` |
| physically-fixed | colors that must not follow the theme (the QR's white, the natural clock's skies) — hex allowed WITH a comment | `--_qr-bg: #ffffff; /* literal: QR quiet zone must be true white */` |
| `/* important: <reason> */` | an `!important` that must win EVERY cascade fight — the three legitimate shapes: bypassing a soma inline style, cross-recipe fusion (the group's radius flattening), reduced-motion kills | `cursor: nwse-resize !important; /* important: wins over the header's inherited grab cursor */` |
| `/* important: <reason> */`| an `!important` that must win EVERY cascade fight — the three legitimate shapes: bypassing a soma inline style, cross-recipe fusion (the group's radius flattening), reduced-motion kills | `cursor: nwse-resize !important; /* important: wins over the header's inherited grab cursor */` |
What does **not** pass as an exception: "it's faster this way", "the token
What does **not** pass as an exception: "it's faster this way", "the token
didn't exist" (create it), "it's just a hover" (that is exactly the
didn't exist" (create it), "it's just a hover" (that is exactly the
| **Focus** | `outline` + `--focus-ring-*` (§32 — ONE model, HCM-safe; the foundation fallback is `:where()`-wrapped so recipes win) | own focus tokens; box-shadow rings (die in HCM) | R-1.5 + forced-colors floor |
| **Focus**| `outline` + `--focus-ring-*` (§32 — ONE model, HCM-safe; the foundation fallback is `:where()`-wrapped so recipes win) | own focus tokens; box-shadow rings (die in HCM) | R-1.5 + forced-colors floor |
| **Field label** | the canonical label role (size-relative, one step below the input; unified weight/color) | redefining `--{c}-label-*` | Field doctrine 2026-07-05 |
| **Field label**| the canonical label role (size-relative, one step below the input; unified weight/color) | redefining `--{c}-label-*`| Field doctrine 2026-07-05 |
| **Size (controls)** | the `--size-{k}-*` bundle (height·font·padding·gap·radius·icon) | re-deriving size→font; consuming none of the bundle | size-bundle test (recipe-css-contract) |
| **Size (controls)**| the `--size-{k}-*` bundle (height·font·padding·gap·radius·icon) | re-deriving size→font; consuming none of the bundle | size-bundle test (recipe-css-contract) |
| **Touch hit-area** | §37: `--touch-target` (44px) under `pointer: coarse` only — AREA ≠ VISUAL (`::before` slop for bare markers) | targets <44ontouchwithoutslop;growingthevisual|archetypes.csscoarserules|
| **Touch hit-area**| §37: `--touch-target` (44px) under `pointer: coarse` only — AREA ≠ VISUAL (`::before` slop for bare markers) | targets <44ontouchwithoutslop;growingthevisual|archetypes.csscoarserules|
| **Portal typography** | anchor `font-family`+`line-height`+`color` on the portaled content root | inheriting (falls to serif in the portal) | rule (LIVE) |
| **Portal typography**| anchor `font-family`+`line-height`+`color` on the portaled content root | inheriting (falls to serif in the portal) | rule (LIVE) |
| **RTL** | **logical** properties (`inline/block`, `inset-inline`) for flow; physical `left/right` ONLY as floating *placement* APIs (EID-3 exception) or where the geometry itself is physical (compass handles, polar arcs, a JS-measured offset); branch on direction with **`:dir(rtl)`** — and then the provider MUST stamp the raw `dir`, or `:dir()` only ever sees the inherited direction ([direction contract](../canon/direction-contract.md)) | physical `padding-left`/… in content flow; a logical anchor paired with a physical `translateX` — the anchor flips, the transform does not; `[dir='rtl'] …`, which misses the common no-attribute case and ignores any nearer re-declaration; accepting the prop, running the chain and never stamping — the maths moves, the paint stays behind; a `:dir()` rule that turns one logical face off and repaints the other — the property had ALREADY mirrored, so that cancels it | RTL-1 · RTL-2 · `npm run rtl:check` |
| **RTL**| **logical** properties (`inline/block`, `inset-inline`) for flow; physical `left/right` ONLY where the geometry itself is physical (compass handles, polar arcs, a JS-measured offset) or as a **placement grid that must NOT mirror** — and that is now a named choice, not an exception: `Position` (physical) vs `LogicalPosition` (`start`/`end`, mirrors), both consts in `eidos/lib/types.ts`, both in [`canon/vocabularies.md`](../canon/vocabularies.md) §Placement grids. **Ask: must it flip for a right-to-left reader?** A strip pinned to `bottom-end` belongs on the trailing edge in both directions; a panel that opens to the physical right because that is where the space is does not. Narrow with `Extract<>`, never re-declare a grid (the logical one was hand-written five times until 2026-08-15). This closes EID-3, which recorded the physical exception in July 2026 and left its doctrine pending; branch on direction with **`:dir(rtl)`** — and then the provider MUST stamp the raw `dir`, or `:dir()` only ever sees the inherited direction ([direction contract](../canon/direction-contract.md)) | physical `padding-left`/… in content flow; a logical anchor paired with a physical `translateX` — the anchor flips, the transform does not; `[dir='rtl'] …`, which misses the common no-attribute case and ignores any nearer re-declaration; accepting the prop, running the chain and never stamping — the maths moves, the paint stays behind; a `:dir()` rule that turns one logical face off and repaints the other — the property had ALREADY mirrored, so that cancels it | RTL-1 · RTL-2 · `npm run rtl:check` |
| **RTL · SVG** | a graphic with a READING axis mirrors (invert the scale's pixel range); a RADIAL one does not. `text-anchor` is LOGICAL: leave it alone when the composition mirrors, force the physical one when it does not — see `eidos/components/chart/README.md` §Direction | mirroring *and* flipping the anchor (they cancel); flipping the anchor on a gutter that never moves (the label walks across the graphic); mirroring y values | eye, in RTL — RTL-1 reads CSS text and cannot see SVG attrs or JS-written inline geometry |
| **RTL · SVG**| a graphic with a READING axis mirrors (invert the scale's pixel range); a RADIAL one does not. `text-anchor` is LOGICAL: leave it alone when the composition mirrors, force the physical one when it does not — see `eidos/components/chart/README.md` §Direction | mirroring _and_ flipping the anchor (they cancel); flipping the anchor on a gutter that never moves (the label walks across the graphic); mirroring y values | eye, in RTL — RTL-1 reads CSS text and cannot see SVG attrs or JS-written inline geometry |
| **i18n** | `eidos.langs.ts('#?key\|fallback')` + key in the catalog | hardcoded strings / `aria-label`s | rule (LIVE) |
| **i18n**| `eidos.langs.ts('#?key\|fallback')` + key in the catalog | hardcoded strings / `aria-label`s | rule (LIVE) |
| **Color (values)** | role tokens `--color-*` / recipe tokens | raw hex/rgb/hsl/oklch | R-2.1/2.6 · R-4.6 |
| **Color (values)**| role tokens `--color-*` / recipe tokens | raw hex/rgb/hsl/oklch | R-2.1/2.6 · R-4.6 |
| `npm run morfo:check` | the **real DOM vs the morfo contract** — navigates each demo and validates emitted `data-*` against the declaration. |
| `npm run morfo:check` | the **real DOM vs the morfo contract** — navigates each demo and validates emitted `data-*` against the declaration. |
| `npm run morfo:vocabulary` | **canonical-verb / vocabulary drift** in morfo events (e.g. `open\|closed`, declared verbs not in `SEMA_VERBS`). Hard-fails CI on declared-verb drift. |
| `npm run morfo:vocabulary` | **canonical-verb / vocabulary drift** in morfo events (e.g. `open\|closed`, declared verbs not in `SEMA_VERBS`). Hard-fails CI on declared-verb drift. |
| `npm run component:audit` | the **acceptance matrix** of [`completion-checklist.md`](./guides/completion-checklist.md) — per-rule severity/applicability across all four layers + recipe + demo. |
| `npm run component:audit` | the **acceptance matrix** of [`completion-checklist.md`](./guides/completion-checklist.md) — per-rule severity/applicability across all four layers + recipe + demo. |
| `npm run perm:check` | **state-transition** bugs — cycles a component through its declared states (via `data-perm-step` annotations) and re-validates morfo after each. Catches reactivity loops and transition-time drift `morfo:check` can't. |
| `npm run perm:check` | **state-transition** bugs — cycles a component through its declared states (via `data-perm-step` annotations) and re-validates morfo after each. Catches reactivity loops and transition-time drift `morfo:check` can't. |
| `npm run smoke` | **runtime / hydration** errors — walks every `+page.svelte` with Playwright and surfaces `pageerror`, `console.error`, missing translation keys, `Context "X" not found`, and `__uix_lang_missing__` markers. Needs `npm run dev` running. HTTP 200 is SSR only; smoke exercises client hydration. |
| `npm run smoke` | **runtime / hydration** errors — walks every `+page.svelte` with Playwright and surfaces `pageerror`, `console.error`, missing translation keys, `Context "X" not found`, and `__uix_lang_missing__` markers. Needs `npm run dev` running. HTTP 200 is SSR only; smoke exercises client hydration. |
| `npm run layer:check` | a **shared visual layer losing a cascade fight** — for every element carrying a layer hook (`data-affix-placement`), asserts the computed `position`, that the stacking token resolved, and that no override slot declared inline computes to nothing (the signature of a custom-property CYCLE). Needs `npm run dev`. Its consumer list is DERIVED from who imports the layer, so a component joins the day it migrates. **Deliberate hole, stated in the script**: geometry. `getComputedStyle` reports the USED value, so an `inset: auto` reads back as pixels (measured: `-1976.7px`) — there is no property-level way to tell a dead `calc()` from an intended value. |
| `npm run docs:check` | **doc-corpus drift** — copied vocabulary counts vs the source consts, phantom fields (the legacy morfo text field; rejected API shapes), dependency claims vs `package.json`, the variant-vocab mirror in `component-audit.ts`, checklist↔audit rule-ID sync, and relative links (warn severity). Guards the "link the canon, never copy it" law of [`docs/authoring.md`](./authoring.md). |
| `scripts/eidos-lint.ts` · `eidos-lint-all.ts` | classifies every `[data-*]` selector in eidos CSS as **morfo-backed / eidos-only / invalid** — drift between the morfo contract and the CSS. (Opt-in safety net; the architectural defense is the typed `semaSelector` builder — see the "Eidos drift defense" rule in CLAUDE.md.) |
| `npm run docs:check` | **doc-corpus drift** — copied vocabulary counts vs the source consts, phantom fields (the legacy morfo text field; rejected API shapes), dependency claims vs `package.json`, the variant-vocab mirror in `component-audit.ts`, checklist↔audit rule-ID sync, and relative links (warn severity). Guards the "link the canon, never copy it" law of [`docs/authoring.md`](./authoring.md). |
| `scripts/eidos-lint.ts` · `eidos-lint-all.ts` | classifies every `[data-*]` selector in eidos CSS as **morfo-backed / eidos-only / invalid** — drift between the morfo contract and the CSS. (Opt-in safety net; the architectural defense is the typed `semaSelector` builder — see the "Eidos drift defense" rule in CLAUDE.md.) |
| `src/uix/eidos/shared-layer-contract.test.ts` (vitest) | the **text half** a browser cannot cover for a shared layer: every zone has a rule, `stretch` stays on the axes it was scoped to, no axis is read without its override slot, no geometry rule keys on the component identity, and each consumer imports the layer / stamps the hook / mints no parallel token / re-asserts `position` when the primitive it composes declares one. It exists for ONE thing the computed check is blind to: `getComputedStyle` of a safe-area slot returns `"0px"` on desktop, so a `:dir(rtl)` remap with one half flipped reads identical to a correct one on every CI machine and only surfaces on a notched phone, sideways, in RTL. |
| `src/uix/contracts.test.ts` (vitest, via `npm run test`) | **catalogue invariants** that keep declarations / recipes coherent as the framework grows. Each fails on drift naming the offender, and excludes the active-dev-track set so it stays green for the maintained catalogue: **VG-8** every morfo is `as const satisfies Morfo`, never `: Morfo` (a `: Morfo` annotation widens the literal so the schema can't check it); **SYS-1 scope-drift** a component shipping an `eidos/components/{c}/` recipe declares `'eidos'` in `scope`; **A31** no per-item membership predicate (`isSelected` / `isItemPressed` / …) doing `.current.includes` (O(N²) — lift a `Set`, use `.has()`); **A30**`inputId` registered with the parent Field in the constructor, not wrapped in a `$effect`; **THEME-SYS-1** overlay z-index references the named `--z-index-overlay-*` scale, never a raw integer. |
| `src/uix/contracts.test.ts` (vitest, via `npm run test`) | **catalogue invariants** that keep declarations / recipes coherent as the framework grows. Each fails on drift naming the offender, and excludes the active-dev-track set so it stays green for the maintained catalogue: **VG-8** every morfo is `as const satisfies Morfo`, never `: Morfo` (a `: Morfo` annotation widens the literal so the schema can't check it); **SYS-1 scope-drift** a component shipping an `eidos/components/{c}/` recipe declares `'eidos'` in `scope`; **A31** no per-item membership predicate (`isSelected` / `isItemPressed` / …) doing `.current.includes` (O(N²) — lift a `Set`, use `.has()`); **A30**`inputId` registered with the parent Field in the constructor, not wrapped in a `$effect`; **THEME-SYS-1** overlay z-index references the named `--z-index-overlay-*` scale, never a raw integer. |
## Codegen — what is generated vs authored
## Codegen — what is generated vs authored
@ -68,8 +70,8 @@ These catch classes of bug that `check` (types) and an HTTP 200 (SSR) miss.
Some surfaces are produced from a source of truth, not hand-maintained. Don't
Some surfaces are produced from a source of truth, not hand-maintained. Don't
| `npm run generate:eidos-css` | the eidos foundation/recipe CSS, from `EidosConfig.recipes`. (`generated/base.css` is output — regenerate, don't hand-edit.) |
| `npm run generate:eidos-css` | the eidos foundation/recipe CSS, from `EidosConfig.recipes`. (`generated/base.css` is output — regenerate, don't hand-edit.) |
The `data-*` contract is **not** a generated doc — it is the morfo itself, validated
The `data-*` contract is **not** a generated doc — it is the morfo itself, validated
@ -93,7 +95,7 @@ and headless tests working:
- **`dom:false`** injects a shared `disabledDom` no-op consumed by Soma / Sema /
- **`dom:false`** injects a shared `disabledDom` no-op consumed by Soma / Sema /
Eidos — there is no silent fallback to direct DOM writes (see
Eidos — there is no silent fallback to direct DOM writes (see
[`arts/adom/README`](../src/arts/adom/README.md) and `src/uix/contracts.ts`).
[`arts/adom/README`](../src/arts/adom/README.md) and `src/uix/contracts.ts`).
- **`ActiveDom`** resolves the *owner*`document` / `window` (`getDocument(node)`
- **`ActiveDom`** resolves the _owner_`document` / `window` (`getDocument(node)`
/ `getWindow(node)`), so it is correct under iframes, popups and happy-dom —
/ `getWindow(node)`), so it is correct under iframes, popups and happy-dom —
not bound to the global `document`.
not bound to the global `document`.
- **Sema is ornamental** — `ActiveUix.events` (the perceptual engine) is optional;
- **Sema is ornamental** — `ActiveUix.events` (the perceptual engine) is optional;