You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/scripts/theming-census.ts

1953 lines
82 KiB

docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/**
* theming-census — how much of each recipe's APPEARANCE can a theme reach
* through the component's own public tokens?
*
* node --import tsx/esm scripts/theming-census.ts # summary + table
* node --import tsx/esm scripts/theming-census.ts --json # machine-readable
* node --import tsx/esm scripts/theming-census.ts --only tabs
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* node --import tsx/esm scripts/theming-census.ts --report # docs/audit/theming/
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* node --import tsx/esm scripts/theming-census.ts --names # naming grammar (D-TH.6)
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
*
* WHY. The recipe contract already says every recipe declares its knobs in
* `lib/recipes/base.ts` (recipe-contract §1, theming §6), and the R-4.x guards
* make sure nothing is a literal. But nothing asserted that a knob is REACHABLE
* by a theme: a recipe that binds every knob to a global primitive
* (`var(--radius-md)`) or to a private (`--_c-radius`) passes every guard and
* can only be themed by moving the whole system. `navigation-menu` shipped that
* way until 2026-08-19 (PLAN-theming.md §0). This script is the baseline and the
* per-component gate of that axis: coverage must go UP, literals must go DOWN,
* and the number must be reproducible by anyone — never quoted from memory.
*
* WHAT COUNTS. A declaration is a KNOB when its property is one of
* `KNOB_PROPS` (appearance: fill, ink, border, radius, shadow, spacing, type,
* size). Layout mechanics (display, position, flex, grid, overflow, …) are not
* knobs. Each knob is classed by WHERE its value comes from:
* public — `var(--{c}-…)` the component's own contract token
* private — `var(--_{c}-…)` a component-internal name (a theme cannot
* name it; fine ONLY if the private derives
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* from a public — the report's §3 per component)
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* system — a transversal system token the recipe contract says a recipe
* CONSUMES rather than owns: state layer, focus ring, depth
* planes, motion, z-bands, opacity, shape, floating gap
* global — any other `var(--…)`: a raw primitive (`--space-*`,
* `--radius-*`, `--color-*`, `--font-size-*`, `--size-*`…)
* literal — no `var(` at all (`8px`, `1.25`, `#fff`)
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* exception — a literal ANNOTATED `/* literal: <reason> *​/` on its own
* declaration: recipe-contract §3's exception valve, the same one
* `component-audit` honours. A signed deviation is not debt.
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
* structural — EVERY knob of a component whose 0 % is its NATURE and not its
* debt, because it has no contract to write: the signed list is
* `STRUCTURAL_COMPONENTS` below.
* Reach = public / (public + private + global + literal). `system`, `exception`
* and `structural` are reported but excluded from the ratio on purpose: the
* first is themeable at the system level by design (recipe-contract §2), the
* second is a deviation the canon already accepted in writing (§3), and the
* third has nothing a theme could name in the first place.
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
*
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* REPORT. `--report` writes the audit under `docs/audit/theming/`: a root
* `README.md` with the whole-catalogue view, and one `{c}.md` per component
* with its analysis AND a proposed correction (the token to declare, its
* value-preserving definition, the TSC scope). The proposal derives names from
* recipe-contract §1 + theming §6.7 — it never invents a name, and it marks
* `⚠ decisión` wherever the doctrine does not decide for us.
*
* LIMITS (honest). A regex over CSS text: a declaration carrying SEVERAL tokens
* is classed by the first class that matches, in order public → private →
* system → global (so `outline: var(--focus-ring-width) solid
* var(--color-primary-border)` reads as `system`); values inside `@keyframes`
* count like any other; another component's public token (`--calendar-*`
* consumed from `range-calendar`) counts as `global`, because the needle is
* this component's own directory name — the report flags those as BORROWED
* separately. It over-reports, never under-reports, so a PASS here is a floor.
* The guard that makes this binding lives in `component-audit` (R-5,
* PLAN-theming.md F0/F3).
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
*/
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import { join, resolve } from 'node:path';
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
import { compileMorfo } from '../src/uix/morfo/compile';
import type { Morfo } from '../src/uix/morfo/types';
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
import { CENSUS_DEBT } from './theming-census-debt.ts';
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const ROOT = resolve('src/uix/eidos/components');
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const REPORT_DIR = resolve('docs/audit/theming');
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const CONTRACT = readFileSync(resolve('src/uix/eidos/lib/recipes/base.ts'), 'utf8').replace(
/\r\n/g,
'\n'
);
const KNOB_PROPS =
uix(anchor-nav): temable — 20 % → 100 %, y el censo aprende los bordes POR LADO 11 claves nuevas (2 → 13). Tenía dos knobs y todo lo demás a pelo: la escala del índice (que ahora lee el bundle, no `--font-size-sm` suelto), el relleno y el radio del enlace, sus tres tintas —reposo, hover y activo—, el peso del activo y LAS DOS TINTAS DEL RAÍL, que es lo que da nombre al componente. EL RAÍL ACTIVO ESTABA INVISIBLE PARA EL CENSO. Su color vive en un `border-inline-start-color`, y `KNOB_PROPS` sólo conocía `border-color` y los shorthands: los ocho longhands por lado (`border-{top,bottom,left,right, block-start,block-end,inline-start,inline-end}-{color,width}`) no eran knobs para la medida. El centinela los mira desde el 2026-08-22 —los añadió cuando prose y nav-tree leyeron muertos—; el censo, no. Corregido: +30 FILAS EN EL CATÁLOGO (5.101 → 5.131 knobs), y esta ficha baja de 20 % a 18 % antes de coserse, que es lo honesto: el censo promete sobre-reportar, nunca infra-reportar. La tinta de reposo del raíl estaba escondida de otra manera: dentro del shorthand `border-inline-start: var(--anchor-nav-rail-width) solid var(--color-border-subtle)`, que el censo clasifica por el PRIMER token que casa —el trazo, ya público— y da la fila por alcanzable. No hizo falta partir el shorthand: basta con nombrar el color dentro. Un susto útil: una coma que faltaba en el contrato rompió el build de tokens, y lo que lo delató fue la SONDA midiendo 0 nodos — la página no renderizaba. El recuento de nodos es el canario, tal como dice el handoff. Gates: sonda 0 diffs en 1.760 valores × 7 estados (8 nodos) · centinela 11/12 con el radio del foco adjudicado (el guard desenfoca a propósito) · censo 100 % · `component:audit` PASS · suite eidos con el rojo conocido ajeno · `rtl:check` 0 · `docs:check` 0 · captura del raíl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/^(background|background-color|background-image|color|border|border-color|border-width|border-radius|border-block|border-inline|border-top|border-bottom|border-left|border-right|border-block-start|border-block-end|border-inline-start|border-inline-end|border-top-color|border-bottom-color|border-left-color|border-right-color|border-block-start-color|border-block-end-color|border-inline-start-color|border-inline-end-color|border-top-width|border-bottom-width|border-left-width|border-right-width|border-block-start-width|border-block-end-width|border-inline-start-width|border-inline-end-width|box-shadow|outline|outline-color|outline-width|padding|padding-inline|padding-block|padding-top|padding-bottom|padding-left|padding-right|padding-inline-start|padding-inline-end|padding-block-start|padding-block-end|gap|row-gap|column-gap|font-size|font-weight|font-family|line-height|letter-spacing|min-block-size|block-size|min-inline-size|inline-size|height|min-height|width|min-width|opacity|fill|stroke|accent-color|caret-color|text-decoration-color|filter|backdrop-filter)$/;
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/** Transversal systems a recipe CONSUMES (themeable at system level — not per component). */
const SYSTEM =
/var\(--(state-(hover|press|selected)|focus-ring[a-z-]*|depth-[a-z-]+|motion-[a-z-]+|duration-[a-z-]+|ease-[a-z-]+|z-index-[a-z-]+|opacity-[a-z-]+|shape-[a-z-]+|floating-gap[a-z-]*|ring-inset-[a-z-]+)\b/;
const INERT = new Set([
'0',
'none',
'auto',
'inherit',
'initial',
'unset',
'transparent',
'currentColor',
'currentcolor',
'revert',
'revert-layer'
]);
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/** recipe-contract §3's exception valve, as written in the canon. */
const ANNOTATED = new RegExp('[/][*][ ]*literal:');
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
export type KnobClass =
| 'public'
| 'private'
| 'system'
| 'global'
| 'literal'
| 'exception'
| 'structural';
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
export interface Knob {
file: string;
line: number;
selector: string;
prop: string;
value: string;
klass: KnobClass;
}
/** A `--_{c}-x: …` declaration found in the component's own CSS. */
export interface PrivateDecl {
name: string;
file: string;
line: number;
selector: string;
value: string;
/** Where the private's own value comes from — the same five classes. */
source: KnobClass;
}
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
export interface CensusRow {
component: string;
knobs: number;
public: number;
private: number;
system: number;
global: number;
literal: number;
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/** A literal carrying its `/* literal: … *​/` annotation — recipe-contract §3. */
exception: number;
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
/** Knobs of a component whose 0 % is its nature — `STRUCTURAL_COMPONENTS`. */
structural: number;
/** public / (public + private + global + literal) — the other three out. */
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
reach: number;
contractKeys: number;
hasSize: boolean;
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/**
* Comments are blanked, not deleted, so `file:line` stays exact and the scanned
* text keeps the same offsets as the source.
*/
function strip(css: string): string {
return css.replace(/\r\n/g, '\n').replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
}
feat(census): los primitivos tipográficos se miden contra su capa — pieza 0 de F2-A D-TH.2-b, la firma que va ANTES del bloque porque cambia el suelo del censo: medirla a mitad de camino falsearía todos los antes/después. `heading` marcaba «36 knobs, 0 % alcanzable» y eso no era deuda: era una lectura equivocada. Su receta resuelve cada eje como `var(--_heading-font-size, var(--style-h2-font-size))` — el privado es el escape POR INSTANCIA que el wrapper escribe desde una prop, y el named style es la superficie de tema, ya pública y viva. Acuñar `--heading-*` para espejarla sería un alias por eje × nivel: la clase que mató la purga del changelog §39. El criterio de qué ES un primitivo tipográfico se midió, no se supuso: o la receta SELECCIONA por `data-style` (`heading`, `text`, `s-text` — el named style es su API) o está atada entera a UN style (`code` → `--style-code-*`, `display` → `--style-hero-*`, `label` → `--style-label-*`). `code-block` queda FUERA a propósito: lee un par de tokens de style para su texto pero posee cromo de caja real, y eso sí es suyo. Detalle que costó una vuelta: la comprobación va ANTES de la de privado. Leer el privado primero puntúa el primitivo entero como inalcanzable cuando es completamente temable — por la capa que lo posee. antes 5.199 knobs · 1.837 públicos (37 %) · 894 privados · 232 sistema después 5.199 knobs · 1.837 públicos (38 %) · 788 privados · 338 sistema **Gate**: 106 knobs pasan de `private` a `system` en los SEIS primitivos y **cero** de los otros 156 se mueve — verificado componente a componente contra la versión en HEAD, no a ojo. Alcance `—` (nada que poseer) para heading, text y display; `s-text` al 100 %; `code` y `label` bajan a 7 y 2 knobs de deuda real. Informe regenerado (171 veredictos a mano intactos), veredicto de heading anotado como ejecutado, y recipe-contract §1 gana el párrafo que fija el criterio — incluida la frontera: un menú que lee `--style-label-font-family` para una etiqueta NO es un primitivo y no entra en el conjunto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/**
* Typographic primitives (D-TH.2, signed 2026-08-20 — option (b) of the
* `heading` sheet). Their theming surface IS the named-style layer, so
* `--style-*` counts as a transversal SYSTEM for them, never as a global that
* they should have minted a token for. Minting `--heading-*` would be one
* alias per axis × level over a layer that is already public and live — the
* class of alias the changelog §39 purge killed.
*
* The criterion is measured, not guessed: either the recipe SELECTS by
* `data-style` (`heading`, `text`, `s-text` — the named style is their API)
* or it is bound wholesale to ONE named style (`code` → `--style-code-*`,
* `display` → `--style-hero-*`, `label` → `--style-label-*`).
*
* `code-block` is deliberately NOT here: it reads a couple of style tokens for
* its text but owns real box chrome (border, padding, surface), and that IS
* its to own.
*/
const TYPOGRAPHIC_PRIMITIVES = new Set(['heading', 'text', 's-text', 'code', 'display', 'label']);
const STYLE_LAYER = /var\(--style-[a-z0-9-]+/;
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el artefacto de `listbox` (§13), pero total. **`lib/calendar-surface.css`** (hook `data-calendar-surface`): - cuatro coordenadas por talla — `padding`, `control-size`, `day-size`, `font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos por debajo del bundle de control. Esa desviación estaba escondida en cuatro bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un privado con otro nombre; ahora se lee en un sitio. - la FORMA del anillo de evento y de la marca de festivo. **Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y `viewport-placement` componen primitivos del sistema, así que declaran sus públicos en el fichero y no tienen entrada de receta. Ésta no puede: su vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config, así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA y la capa posee sólo lo que una entrada de receta no sabe expresar. **El defecto que la justificaba, medido**: `--calendar-event-shadow` y `--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]` (audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`, `month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo: variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de `commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por `--_calendar-surface-accent`, que cada superficie alimenta con su propio forward de paleta THM-2. **Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker` y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers (`date-picker-calendar`, `-month-view`, `-year-view`, `date-range-picker-calendar`) y un panel portalado no hereda nada del root del picker. Con sólo los cuatro standalone sellando, ambos quedaban con `--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era enganchar la capa a las cuatro identidades de componente: eso viola la regla 1 de capas compartidas, y la respuesta correcta es un sello por wrapper. computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) · year-grid (3.451) · date-picker (464) · date-range-picker (406). `calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO código dan 24 en los mismos nodos y las mismas dos propiedades. Los «missing node» son el propio sello entrando en la clave. **Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los selectores month/year es una moneda al aire — `[data-calendar-month-select][data-button]` (0,2,0) empata con `[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel en un lado ⇒ decisión. De paso, `calendar-select.css` deja de puentear un privado que sólo `[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el fallback, clavado a md fuera cual fuera la talla. **El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en `theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global 43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus consumidores puentean por privados, otra forma, y mueve diez componentes de golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO el catálogo— así que siguen leyendo 0 %. `recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo miraba la receta, y una capa que comparte prefijo con un componente la hacía fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo. eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) · audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) · rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el reformateo en masa que se coló en cuatro README, el test y el censo (churn ajeno, no mío) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/**
* The vocabulary a SHARED LAYER publishes, and who consumes it (D-TH.2-b
* precedent, extended 2026-08-21 with the `calendar-surface` design).
*
* Same argument as the typographic primitives: when the layer owns an axis,
* the consumer's theming surface for that axis IS the layer, so reading
* `--{layer-vocabulary}-*` is reaching a live public surface — not a raw
* global the component should have minted a token for. Minting one would be
* the parallel vocabulary the layer doctrine forbids, AND it would outrank the
* layer for every other consumer.
*
* Until this existed the census PENALISED doing the correct thing:
* `range-calendar`, `month-grid` and `year-grid` mint zero tokens and read
* 114 / 77 / 77 references from `--calendar-*` — measured — and all three
* scored 0 % reach.
*
* DELIBERATELY not registered yet: `list-surface`. Its consumers bridge
* through PRIVATES (`--_listbox-item-*: var(--list-item-*)`), which is a
* different shape than reading the layer directly, and flipping it moves ten
* components at once — a separate decision (next-features §13).
*/
const LAYER_VOCABULARY: { layer: string; consumers: Set<string>; needle: RegExp }[] = [
{
layer: 'calendar-surface',
consumers: new Set(['range-calendar', 'month-grid', 'year-grid', 'date-picker']),
needle: /var\(\s*--calendar-[a-z0-9-]+/
}
];
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
/**
* A component whose 0 % is its NATURE, not its debt (next-features §13).
*
* Same shape as `LAYER_VOCABULARY` one floor down — the list AND the reason per
* component, so the contract lives IN the artifact instead of in a session's
* memory. The class is per COMPONENT, all of its knobs: what was measured is
* the component, not one declaration.
*
* Measured one by one on 2026-08-22 and SIGNED in the §5 verdict of each sheet
* (`docs/audit/theming/{c}.md`): the five read 0 % reach and NONE of them has a
* contract to write. While the census counted them like a component carrying
* real debt, the global figure lied downwards and F3's gate («census 100 %»)
* was unreachable BY CONSTRUCTION.
*
* Their knobs are still counted and still listed — a structural knob is a knob
* — they only leave the ratio's DENOMINATOR, exactly like `system` does.
*
* This is NOT a drawer for «this one is hard»: every entry is a written §5
* verdict, and a component that gains a real theme surface leaves the list.
*/
const STRUCTURAL_COMPONENTS = new Map<string, string>([
// Its only global is `var(--box-width, 100%)`, BORROWED from `box` (the sheet
// already marks it ⤴): `aspect-ratio` selects `[data-box][data-aspect-ratio]`,
// and `--aspect-ratio` is a per-instance value channel, not a theme surface.
[
'aspect-ratio',
'es una FACETA de `box`, no un componente: el eje lo posee `box` y su knob viene prestado (⤴); `--aspect-ratio` es canal de valor por instancia'
],
// The `1px` × 2 of the canonical sr-only technique on `[data-text-blur-sr]`.
[
'text-blur',
'sus dos knobs son el `1px` de la técnica sr-only: receta de accesibilidad idéntica en todo el catálogo, no estética'
],
// The reveal gate's `opacity` — motion-channel mechanics.
[
'cascade',
'su knob es el `opacity` del gate antiparpadeo: mecánica del canal de motion, cuyo valor tematizable vive en `EidosConfig.motion`'
],
[
'motion',
'igual que `cascade`: el `opacity` del gate `[data-animation-pending]` es mecánica del canal, tematizable desde `EidosConfig.motion`'
],
// `max-content` × 2 on the popover that hosts the calendar: a composition fix
// with ONE correct value. Its chrome comes from `field` / `date-field`.
[
'date-picker',
'sus dos knobs son la corrección `max-content` del pie del popover — un único valor correcto; su cromo vive en `field`, `calendar` y `picker-shell`'
]
]);
feat(census): los primitivos tipográficos se miden contra su capa — pieza 0 de F2-A D-TH.2-b, la firma que va ANTES del bloque porque cambia el suelo del censo: medirla a mitad de camino falsearía todos los antes/después. `heading` marcaba «36 knobs, 0 % alcanzable» y eso no era deuda: era una lectura equivocada. Su receta resuelve cada eje como `var(--_heading-font-size, var(--style-h2-font-size))` — el privado es el escape POR INSTANCIA que el wrapper escribe desde una prop, y el named style es la superficie de tema, ya pública y viva. Acuñar `--heading-*` para espejarla sería un alias por eje × nivel: la clase que mató la purga del changelog §39. El criterio de qué ES un primitivo tipográfico se midió, no se supuso: o la receta SELECCIONA por `data-style` (`heading`, `text`, `s-text` — el named style es su API) o está atada entera a UN style (`code` → `--style-code-*`, `display` → `--style-hero-*`, `label` → `--style-label-*`). `code-block` queda FUERA a propósito: lee un par de tokens de style para su texto pero posee cromo de caja real, y eso sí es suyo. Detalle que costó una vuelta: la comprobación va ANTES de la de privado. Leer el privado primero puntúa el primitivo entero como inalcanzable cuando es completamente temable — por la capa que lo posee. antes 5.199 knobs · 1.837 públicos (37 %) · 894 privados · 232 sistema después 5.199 knobs · 1.837 públicos (38 %) · 788 privados · 338 sistema **Gate**: 106 knobs pasan de `private` a `system` en los SEIS primitivos y **cero** de los otros 156 se mueve — verificado componente a componente contra la versión en HEAD, no a ojo. Alcance `—` (nada que poseer) para heading, text y display; `s-text` al 100 %; `code` y `label` bajan a 7 y 2 knobs de deuda real. Informe regenerado (171 veredictos a mano intactos), veredicto de heading anotado como ejecutado, y recipe-contract §1 gana el párrafo que fija el criterio — incluida la frontera: un menú que lee `--style-label-font-family` para una etiqueta NO es un primitivo y no entra en el conjunto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
function classify(
value: string,
pubNeedle: string,
privNeedle: string,
component?: string
): KnobClass {
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (value.includes(pubNeedle)) return 'public';
feat(census): los primitivos tipográficos se miden contra su capa — pieza 0 de F2-A D-TH.2-b, la firma que va ANTES del bloque porque cambia el suelo del censo: medirla a mitad de camino falsearía todos los antes/después. `heading` marcaba «36 knobs, 0 % alcanzable» y eso no era deuda: era una lectura equivocada. Su receta resuelve cada eje como `var(--_heading-font-size, var(--style-h2-font-size))` — el privado es el escape POR INSTANCIA que el wrapper escribe desde una prop, y el named style es la superficie de tema, ya pública y viva. Acuñar `--heading-*` para espejarla sería un alias por eje × nivel: la clase que mató la purga del changelog §39. El criterio de qué ES un primitivo tipográfico se midió, no se supuso: o la receta SELECCIONA por `data-style` (`heading`, `text`, `s-text` — el named style es su API) o está atada entera a UN style (`code` → `--style-code-*`, `display` → `--style-hero-*`, `label` → `--style-label-*`). `code-block` queda FUERA a propósito: lee un par de tokens de style para su texto pero posee cromo de caja real, y eso sí es suyo. Detalle que costó una vuelta: la comprobación va ANTES de la de privado. Leer el privado primero puntúa el primitivo entero como inalcanzable cuando es completamente temable — por la capa que lo posee. antes 5.199 knobs · 1.837 públicos (37 %) · 894 privados · 232 sistema después 5.199 knobs · 1.837 públicos (38 %) · 788 privados · 338 sistema **Gate**: 106 knobs pasan de `private` a `system` en los SEIS primitivos y **cero** de los otros 156 se mueve — verificado componente a componente contra la versión en HEAD, no a ojo. Alcance `—` (nada que poseer) para heading, text y display; `s-text` al 100 %; `code` y `label` bajan a 7 y 2 knobs de deuda real. Informe regenerado (171 veredictos a mano intactos), veredicto de heading anotado como ejecutado, y recipe-contract §1 gana el párrafo que fija el criterio — incluida la frontera: un menú que lee `--style-label-font-family` para una etiqueta NO es un primitivo y no entra en el conjunto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// BEFORE the private check, and on purpose: a typographic primitive writes
// `var(--_heading-font-size, var(--style-h2-font-size))`, where the private
// is the per-INSTANCE escape hatch the wrapper sets from a prop and the
// named style is the THEMING surface. Reading the private first would score
// the whole primitive as unreachable when it is, in fact, fully themeable —
// through the layer that owns it.
if (component && TYPOGRAPHIC_PRIMITIVES.has(component) && STYLE_LAYER.test(value))
return 'system';
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el artefacto de `listbox` (§13), pero total. **`lib/calendar-surface.css`** (hook `data-calendar-surface`): - cuatro coordenadas por talla — `padding`, `control-size`, `day-size`, `font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos por debajo del bundle de control. Esa desviación estaba escondida en cuatro bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un privado con otro nombre; ahora se lee en un sitio. - la FORMA del anillo de evento y de la marca de festivo. **Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y `viewport-placement` componen primitivos del sistema, así que declaran sus públicos en el fichero y no tienen entrada de receta. Ésta no puede: su vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config, así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA y la capa posee sólo lo que una entrada de receta no sabe expresar. **El defecto que la justificaba, medido**: `--calendar-event-shadow` y `--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]` (audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`, `month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo: variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de `commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por `--_calendar-surface-accent`, que cada superficie alimenta con su propio forward de paleta THM-2. **Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker` y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers (`date-picker-calendar`, `-month-view`, `-year-view`, `date-range-picker-calendar`) y un panel portalado no hereda nada del root del picker. Con sólo los cuatro standalone sellando, ambos quedaban con `--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era enganchar la capa a las cuatro identidades de componente: eso viola la regla 1 de capas compartidas, y la respuesta correcta es un sello por wrapper. computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) · year-grid (3.451) · date-picker (464) · date-range-picker (406). `calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO código dan 24 en los mismos nodos y las mismas dos propiedades. Los «missing node» son el propio sello entrando en la clave. **Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los selectores month/year es una moneda al aire — `[data-calendar-month-select][data-button]` (0,2,0) empata con `[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel en un lado ⇒ decisión. De paso, `calendar-select.css` deja de puentear un privado que sólo `[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el fallback, clavado a md fuera cual fuera la talla. **El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en `theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global 43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus consumidores puentean por privados, otra forma, y mueve diez componentes de golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO el catálogo— así que siguen leyendo 0 %. `recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo miraba la receta, y una capa que comparte prefijo con un componente la hacía fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo. eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) · audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) · rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el reformateo en masa que se coló en cuatro README, el test y el censo (churn ajeno, no mío) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// Same reasoning, one layer up: a shared layer's vocabulary read by one of
// its declared consumers IS that consumer's theming surface for the axis.
if (component && LAYER_VOCABULARY.some((l) => l.consumers.has(component) && l.needle.test(value)))
return 'system';
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (value.includes(privNeedle)) return 'private';
if (SYSTEM.test(value)) return 'system';
if (value.includes('var(--')) return 'global';
return 'literal';
}
function recipeBlock(c: string): string | null {
uix(avatar): temable — 75 % → 90 %, y su contrato era INVISIBLE para los dos instrumentos Protocolo PLAN-theming §7 sobre `avatar` (incluye `AvatarGroup`, que comparte receta y fichero). Censo 75 % → **90 %**, contrato 84 → 88 claves, centinela **83/88** con 5 adjudicadas. El default NO se mueve. ## Lo primero: el instrumento no veía el contrato La entrada `avatar` de `recipes/base.ts` es la ÚNICA construida por una IIFE (un helper local genera sus 24 ámbitos compuestos), así que su mapa vive en el `return {`, un tabulador más adentro. Consecuencia medida: - el censo la leía como «sin entrada en base.ts» — 84 claves invisibles; - el centinela **moría** con `no recipe block for avatar`: el componente no se podía medir, y su gate no significaba nada. Los dos lectores leen ya la IIFE (dedentan el `return`). Sin este arreglo, todo lo demás de este commit habría sido humo. ## Lo que entra al contrato (6 claves) `group-overlap-{xs,sm,md,lg,xl,xxl}` — la escala de solape de `AvatarGroup`. Había UNA clave, `--avatar-group-overlap`, y la receta la RE-DECLARABA en seis bloques `[data-size]`: sentada en el elemento, ganaba siempre al `:root` donde escribe un tema. Medido: `37px` desde el asiento del tema no movía el margen; el mismo valor escrito sobre el nodo, sí. Ahora el paso viaja por `--_avatar-group-overlap` y los seis alcanzan desde `:root` (−8,4 · −11,2 · −14 · −16,8 · −22,4 · −33,6 px → 37 px, uno a uno). ## Lo que se retira (2 declaraciones muertas, diff 0 las dos) - `group-max`: el envoltorio escribía `--avatar-group-max` INLINE y la receta declaraba su default `99`. **No lo leía nadie** — el tope se aplica con `data-has-max` + `:nth-child(n + M)` porque una variable no entra en `:nth-child()`, y lo dice el propio comentario del CSS. Retirado de los dos sitios; el `+3` del grupo sigue exactamente donde estaba. - el respaldo `, white` de `--_avatar-badge-fg`: el contrato ya declara `--avatar-badge-color-custom-contrast: white`, así que era inalcanzable y sólo podía envejecer contra su token. Comprobado en la rama custom: la tinta sigue computando `rgb(255, 255, 255)`. ## Seis literales firmados (salen del ratio) Los cuatro `100 %` de Image y Fallback son IDENTIDAD — la parte ES la superficie del avatar, no una talla propia — y los dos `line-height: 1` mantienen el glifo centrado por la caja flex. ## Lo que se queda privado (el techo real es 90 %) `--_avatar-bg`, `--_avatar-fg` y `--_avatar-badge-bg` son un CONMUTADOR: cambian de fuente con la variante (solid · soft · outline) y su valor sale del forward de paleta THM-2 que la capa de color alimenta por instancia desde `[data-color]`. Un público encima dejaría que un tema los fijara y matara el `color=` de cada avatar. ## Artefactos - Sonda: **0 diffs** en TRES bases — demo de avatar con insignia y anillo (576 valores), la misma en modo fallback (384) y demo de AvatarGroup (3.072). - Capturas 2× antes/después de las dos demos: **idénticas al byte**. - Centinela R-5.4: 83/88, 5 adjudicadas con su medida en el ledger (`size-xxl` y `font-size-xxl` viven en el paso `xxl`, que el barrido del guard no alcanza — el límite que `metrics` ya registró; `radius-none` y `ring-width-sm` son «sólo el paso en vigor pinta»; `group-overlap-xxl` junta las dos cosas). - El guard aprende `avatar`: la insignia y el anillo son OPT-IN, el barrido de `data-variant` destapa las 16 claves `*-outline-*`, y `AvatarGroup` se mide en su propia ruta (21 muertos → 5). - `eidos-lint` 0 invalid · `vitest src/uix/eidos` sin rojos nuevos (el conocido `skin-media-player`) · `rtl:check` 0 · `docs:check` 0 · `npm run check` COMPLETED sin un solo error en `avatar`. - README «Talla y tema» + pestaña `Tokens` (88 filas, verificada en el navegador con `networkidle` y clic real). ## Lo que NO se arregla aquí (anotado) - **R-5.3 preexistente**: cuatro claves `*-color-custom*` donde la gramática exige `fg`. Son escotillas que el envoltorio escribe INLINE, así que renombrarlas cambia el contrato público de tres canales: se lista, el audit sigue en NEEDS-WORK por esto y sólo por esto. - **Falso POSITIVO nuevo del centinela** (next-features §13): escribe el token en `:root` y también sobre cada nodo, y para una propiedad personalizada que la receta re-declara en el ELEMENTO ese inline gana justo lo que un tema pierde. Es lo que hacía que `--avatar-group-overlap` leyera vivo. - **El barrido de tallas del guard para en `xl`**: añadir `xxl` dejaría STALE las seis excepciones de `metrics`, así que se hace con la re-verificación del ledger entero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// `avatar` is the ONLY entry whose value is an IIFE (a local matrix helper
// generates its 24 composite scopes), so its map lives in the `return {` one
// tab deeper and this reader saw NO contract for it — 84 keys reported as
// «sin entrada en base.ts». Read the returned map and dedent it once; the
// key regexes below work unchanged.
const iife = CONTRACT.indexOf('\n\t' + c + ': ((): RecipeTokenMap => {');
if (iife >= 0) {
const from = CONTRACT.indexOf('\n\t\treturn {', iife);
return CONTRACT.slice(from, CONTRACT.indexOf('\n\t\t};', from)).replace(/^\t/gm, '');
}
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const a = CONTRACT.indexOf("\n\t'" + c + "': {");
const b = CONTRACT.indexOf('\n\t' + c + ': {');
const start = a >= 0 ? a : b;
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (start < 0) return null;
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const end = CONTRACT.indexOf('\n\t},', start);
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
return CONTRACT.slice(start, end < 0 ? undefined : end);
}
function contractKeysFor(c: string): number {
const block = recipeBlock(c);
if (block === null) return 0;
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
return (block.match(/^\t\t'?[a-z0-9-]+'?\s*:/gm) ?? []).length;
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/** Every top-level key of the recipe block — public AND `_private` forwards. */
function contractKeyNames(c: string): string[] {
const block = recipeBlock(c);
if (block === null) return [];
return [...block.matchAll(/^\t\t'?(_?[a-z0-9-]+)'?\s*:/gm)].map((m) => m[1]);
}
export interface Scan {
row: CensusRow;
knobs: Knob[];
privates: PrivateDecl[];
/** Privates the CSS consumes but never declares — from `base.ts` or an inline style. */
privatesFromContract: string[];
files: string[];
}
/** Every component directory — used to spot a token BORROWED from another recipe. */
const COMPONENT_DIRS = readdirSync(ROOT).filter((d) => statSync(join(ROOT, d)).isDirectory());
export function scanComponent(dir: string): Scan | null {
const d = join(ROOT, dir);
if (!statSync(d).isDirectory()) return null;
const files = readdirSync(d).filter((f) => f.endsWith('.css'));
if (files.length === 0) return null;
const pubNeedle = 'var(--' + dir + '-';
const privNeedle = 'var(--_' + dir + '-';
const row: CensusRow = {
component: dir,
knobs: 0,
public: 0,
private: 0,
system: 0,
global: 0,
literal: 0,
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
exception: 0,
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
structural: 0,
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
reach: 0,
contractKeys: contractKeysFor(dir),
hasSize: false
};
const knobs: Knob[] = [];
const privates: PrivateDecl[] = [];
const usedPrivates = new Set<string>();
for (const file of files) {
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// The stripped body is what gets CLASSIFIED (a comment must not read as a
// value), but the annotation of recipe-contract §3 lives IN a comment, so
// the raw lines are kept beside it. `strip` blanks comments in place, so
// the two are line-for-line aligned.
const raw = readFileSync(join(d, file), 'utf8').replace(/\r\n/g, '\n');
const rawLines = raw.split('\n');
const body = strip(raw);
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (body.includes('data-size')) row.hasSize = true;
const lineStarts: number[] = [0];
for (let i = 0; i < body.length; i++) if (body.charCodeAt(i) === 10) lineStarts.push(i + 1);
const lineAt = (idx: number) => {
let lo = 0;
let hi = lineStarts.length - 1;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (lineStarts[mid] <= idx) lo = mid;
else hi = mid - 1;
}
return lo + 1;
};
// Block-wise so every declaration carries the selector it lives under.
// `[^{}]` cannot cross a brace, so an @media's inner rules are matched
// individually and the at-rule prelude never leaks into the selector.
for (const block of body.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
// A statement at-rule (`@import '…';`) sits in the same run as the next
// selector — keep what follows its `;`, or the whole rule is skipped and
// its knobs vanish (measured: color-picker lost 2).
const selector = (block[1].split(';').pop() ?? '').trim().replace(/\s+/g, ' ');
if (selector.startsWith('@')) continue; // @property / @font-face: no knobs
const inner = block[2];
const innerStart = (block.index ?? 0) + block[1].length + 1;
// Not anchored to the line start: a one-line rule (`[x] { padding: 8px; }`)
// counts too — the first version of this regex missed it, and a mutation
// test (PLAN-theming.md §7.5) is what caught it.
for (const m of inner.matchAll(/(?<=^\s*|;\s*)(--[a-z0-9_-]+|[a-z-]+)\s*:\s*([^;]+);/gm)) {
const prop = m[1];
const val = m[2].trim().replace(/\s+/g, ' ');
const line = lineAt(innerStart + (m.index ?? 0));
for (const p of val.matchAll(/var\(\s*(--_[a-z0-9-]+)/g)) usedPrivates.add(p[1]);
if (prop.startsWith('--')) {
uix(button): temable — 55 % → 91 %, y la cascada de paleta ANULA 419 claves Una clave nueva y 17 declaraciones anotadas. Al medirlo salió lo más gordo del eje hasta hoy: 56 de las 106 claves públicas de este componente no pintan nada, y no por deuda suya. Acuñado lo único que faltaba: EL PASO `full` DE LA ESCALA DE RADIO — `[data-rounded='full']` leía el primitivo global a pelo, así que la forma que más se retoca (la píldora) era el único escalón fuera del contrato. Diecisiete declaraciones pasan a excepción firmada, de dos clases: IDENTIDAD (`fit-content` × 2 —un botón ES su contenido—, el `100 %` de `[data-block]` y los cuatro `1px` de las cajas sr-only) y AUSENCIA (los `transparent` de `soft`, `outline`, `ghost` y `plain`: la variante ES la ausencia de cromo, y ponerle fondo a un `ghost` es pedir un `soft`). Diez claves de `transparent` habrían sido justo el antipatrón que la doctrina avisa: agrupa por VALOR. Para que la ausencia cuente, EL CENSO APRENDE LA VÁLVULA UN PISO MÁS ABAJO: un PRIVADO cuyo valor es un literal CON su razón escrita es una ausencia firmada, no deuda. Sin eso, cuatro privados marcaban «no deriva» y seis knobs leían inalcanzables mientras todas las demás ramas del mismo privado leen un público. ⚠⚠ EL HALLAZGO: LA CASCADA DE PALETA ANULA LOS TONOS DEL COMPONENTE. El último bloque del forward, `[data-button][data-color]`, resuelve `--button-palette-*` desde el `--palette-*` GLOBAL —que `[data-color='{tono}']` llena desde `--color-{tono}-*`— y gana a las reglas por tono por ORDEN, con la misma especificidad (0,2,0). Medido sobre `data-color='risk'`: `--button-risk-solid` no mueve nada, ni en `:root` ni en el nodo, mientras `--palette-solid` y `--color-risk-solid` en el nodo repintan. Las siete `primary-*` son el RESPALDO de esa misma regla y sólo actúan SIN `data-color`. No es de este componente: 49 recetas emiten el mismo forward genérico y el contrato tiene 419 CLAVES DE TONO. Medido igual en `badge` y `callout`. El arreglo cabe en el orden de emisión —el genérico ANTES de los de tono— y no movería un píxel, porque el valor por defecto de `--button-risk-solid` ES `var(--color-risk-solid)`; pero cambia el contrato de la jaula del color (un ancestro que inyecte `--palette-*` dejaría de pisar un tono semántico). Es firma. → §13. El techo restante son dos filas de canal de valor: `--_button-fill-finish`, el acabado de degradado que el generador deriva por instancia. Un error de método, anotado porque casi cuela: anclé la clave nueva en la primera línea `'radius-xl'` del fichero y aterrizó en `meter`. Lo delató el navegador (`--button-radius-full` sin definir ⇒ radio 0). Una inserción se ancla en el BLOQUE del componente, nunca en una línea de clave. Instrumento: el guard aprende `sweepAttr` (barrer una variante mientras mide) y a estampar el TONO que la clave nombra; y el fichero de excepciones gana PATRONES, para que una razón arquitectónica se escriba una vez y no 56. Gates: sonda 0 diffs (416 valores; la demo monta UN botón) · equivalencia del paso `full` comprobada aparte · centinela 34/106 con todo lo demás adjudicado · censo 91 % · `component:audit` PASS · suite eidos con el rojo conocido ajeno · `rtl:check` 0 · `docs:check` 0 · `check` sin errores propios · captura de las seis variantes + píldora + tono `risk`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (prop.startsWith('--_' + dir + '-')) {
// recipe-contract §3 again, one level down: a private whose value is a
// literal WITH its written reason is a signed absence, not debt. Button
// declares `--_button-bg: transparent` on the variants that ARE the absence
// of chrome; without this those privates score as non-derived and six knobs
// read unreachable while every OTHER branch of the same private reads a
// public. Same valve as the knob below, same requirement: the reason is
// written on the declaration.
let source = classify(val, pubNeedle, privNeedle, dir);
if (source === 'literal' && ANNOTATED.test(rawLines[line - 1] ?? ''))
source = 'exception';
privates.push({ name: prop, file, line, selector, value: val, source });
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
continue;
}
if (!KNOB_PROPS.test(prop) || INERT.has(val)) continue;
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
// A structural component is structural WHOLE: classifying its knobs one
// by one would ask a question its §5 verdict already answered.
let klass: KnobClass = STRUCTURAL_COMPONENTS.has(dir)
? 'structural'
: classify(val, pubNeedle, privNeedle, dir);
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// recipe-contract §3: a deviation annotated on its own declaration is
// a SIGNED exception, not drift — the same valve `component-audit`
// honours for R-4.x. The census used to count all 81 of them as debt
// because `strip` blanks the comment before anything is classified.
// The whole declaration counts as "its own line": a value broken over
// several lines carries the note at the end.
if (klass === 'literal') {
const endLine = lineAt(innerStart + (m.index ?? 0) + m[0].length - 1);
for (let ln = line; ln <= endLine; ln++)
if (ANNOTATED.test(rawLines[ln - 1] ?? '')) {
klass = 'exception';
break;
}
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
row[klass]++;
knobs.push({ file, line, selector, prop, value: val, klass });
}
}
}
uix(theming): el censo aprende que un privado DERIVADO sí alcanza — 50 % → 56 % Seis puntos de alcance sin tocar una línea de componente: **la medición estaba mal, no el código**. EL DEFECTO. F0 encargaba «detección "privado que deriva de público" (leer `--_c-x: var(--c-y)` en la misma receta y reclasificar)». Se implementó a medias: el censo CALCULA `derives` y lo imprime en la columna «¿deriva de un público?» de la §3 de cada ficha, pero **el contador nunca lo usó** — esos knobs seguían puntuando como deuda. Medido: **240 privados en 59 componentes** estaban en ese estado, es decir, haciendo exactamente lo que la doctrina prescribe («privados sólo si derivan de públicos») y penalizados por ello. Lo destapó tokenizar `avatar`: sus 17 privados YA leían públicos (`--_avatar-size: var(--avatar-size-md)`), así que no había nada que tokenizar — había que arreglar el instrumento. LA CORRECCIÓN. Segunda pasada tras recoger privados y knobs: un knob que lee `var(--_c-x)` pasa a `public` si ese privado alcanza un público. El cierre es **TRANSITIVO** (`--_a: calc(var(--_b) * .3)` alcanza si `--_b` alcanza) con conjunto de visitados para que un ciclo no lo cuelgue. **Muta-prueba** (obligatoria, memoria `a-guard-that-inspects-nothing-passes`): apuntar `--_avatar-radius` al primitivo crudo baja avatar de 75 % a 72 % y el knob vuelve a `private`; revertirlo lo devuelve a 75 % y deja el árbol idéntico. EFECTO MEDIDO: - Global **50 % → 56 %** · privados 625 → 341 · al 100 % 10 → 14 · <20 % 22 → 20. - `avatar` 36 % → **75 %**, `picker-shell` 76 % → **95 %**, `menu-dial` 25 % → 38 %, y otras 51 fichas con cifras nuevas — **ninguna por cambio de código**. - Los READMEs de `picker-shell` y `menu-dial` se actualizan con la cifra nueva y con la razón, para que nadie crea que el componente cambió. ⚠ **Cualquier cifra anterior a este commit no es comparable con las de ahora.** Queda escrito en el handoff. Y un hueco de F0 que sale al hacerlo, registrado en §13: **el test del SUELO que el plan pedía NO EXISTE** («alcance global ≥ el de hoy y literales ≤ 614 — el número sube o el test falla»). Hoy nada impide que el alcance BAJE entre sesiones: sólo se vería mirando la cifra a mano. Con el censo ya endurecido, es el momento de escribirlo. Gates: `component:audit` 162 PASS / 4 NEEDS-WORK (preexistentes) · `--names` DESVIADAS 0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · el censo compila. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
// ── Second pass: a private that DERIVES from a public is reachable ──
// F0 asked for this and it was only ever printed in the report's §3, never
// applied to the count: a recipe that writes `--_c-x: var(--c-y)` and reads
// `var(--_c-x)` is doing exactly what the doctrine prescribes (privates are
// fine IF they derive from a public), yet every such knob scored as debt.
// Measured 2026-08-22: 240 privates across 59 components were in that state.
// The closure is TRANSITIVE — `--_a: calc(var(--_b) * .3)` reaches a public
// whenever `--_b` does — with a visited set so a cycle cannot hang it.
const declBySource = new Map<string, string[]>();
for (const pv of privates) declBySource.set(pv.name, [...(declBySource.get(pv.name) ?? []), pv.source]);
const privValues = new Map<string, string[]>();
for (const pv of privates) privValues.set(pv.name, [...(privValues.get(pv.name) ?? []), pv.value]);
const derivesCache = new Map<string, boolean>();
const derivesFromPublic = (name: string, seen = new Set<string>()): boolean => {
if (derivesCache.has(name)) return derivesCache.get(name)!;
if (seen.has(name)) return false;
seen.add(name);
const sources = declBySource.get(name);
if (!sources || sources.length === 0) return false;
let ok = sources.every((s) => {
uix(button): temable — 55 % → 91 %, y la cascada de paleta ANULA 419 claves Una clave nueva y 17 declaraciones anotadas. Al medirlo salió lo más gordo del eje hasta hoy: 56 de las 106 claves públicas de este componente no pintan nada, y no por deuda suya. Acuñado lo único que faltaba: EL PASO `full` DE LA ESCALA DE RADIO — `[data-rounded='full']` leía el primitivo global a pelo, así que la forma que más se retoca (la píldora) era el único escalón fuera del contrato. Diecisiete declaraciones pasan a excepción firmada, de dos clases: IDENTIDAD (`fit-content` × 2 —un botón ES su contenido—, el `100 %` de `[data-block]` y los cuatro `1px` de las cajas sr-only) y AUSENCIA (los `transparent` de `soft`, `outline`, `ghost` y `plain`: la variante ES la ausencia de cromo, y ponerle fondo a un `ghost` es pedir un `soft`). Diez claves de `transparent` habrían sido justo el antipatrón que la doctrina avisa: agrupa por VALOR. Para que la ausencia cuente, EL CENSO APRENDE LA VÁLVULA UN PISO MÁS ABAJO: un PRIVADO cuyo valor es un literal CON su razón escrita es una ausencia firmada, no deuda. Sin eso, cuatro privados marcaban «no deriva» y seis knobs leían inalcanzables mientras todas las demás ramas del mismo privado leen un público. ⚠⚠ EL HALLAZGO: LA CASCADA DE PALETA ANULA LOS TONOS DEL COMPONENTE. El último bloque del forward, `[data-button][data-color]`, resuelve `--button-palette-*` desde el `--palette-*` GLOBAL —que `[data-color='{tono}']` llena desde `--color-{tono}-*`— y gana a las reglas por tono por ORDEN, con la misma especificidad (0,2,0). Medido sobre `data-color='risk'`: `--button-risk-solid` no mueve nada, ni en `:root` ni en el nodo, mientras `--palette-solid` y `--color-risk-solid` en el nodo repintan. Las siete `primary-*` son el RESPALDO de esa misma regla y sólo actúan SIN `data-color`. No es de este componente: 49 recetas emiten el mismo forward genérico y el contrato tiene 419 CLAVES DE TONO. Medido igual en `badge` y `callout`. El arreglo cabe en el orden de emisión —el genérico ANTES de los de tono— y no movería un píxel, porque el valor por defecto de `--button-risk-solid` ES `var(--color-risk-solid)`; pero cambia el contrato de la jaula del color (un ancestro que inyecte `--palette-*` dejaría de pisar un tono semántico). Es firma. → §13. El techo restante son dos filas de canal de valor: `--_button-fill-finish`, el acabado de degradado que el generador deriva por instancia. Un error de método, anotado porque casi cuela: anclé la clave nueva en la primera línea `'radius-xl'` del fichero y aterrizó en `meter`. Lo delató el navegador (`--button-radius-full` sin definir ⇒ radio 0). Una inserción se ancla en el BLOQUE del componente, nunca en una línea de clave. Instrumento: el guard aprende `sweepAttr` (barrer una variante mientras mide) y a estampar el TONO que la clave nombra; y el fichero de excepciones gana PATRONES, para que una razón arquitectónica se escriba una vez y no 56. Gates: sonda 0 diffs (416 valores; la demo monta UN botón) · equivalencia del paso `full` comprobada aparte · centinela 34/106 con todo lo demás adjudicado · censo 91 % · `component:audit` PASS · suite eidos con el rojo conocido ajeno · `rtl:check` 0 · `docs:check` 0 · `check` sin errores propios · captura de las seis variantes + píldora + tono `risk`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (s === 'public' || s === 'exception') return true;
uix(theming): el censo aprende que un privado DERIVADO sí alcanza — 50 % → 56 % Seis puntos de alcance sin tocar una línea de componente: **la medición estaba mal, no el código**. EL DEFECTO. F0 encargaba «detección "privado que deriva de público" (leer `--_c-x: var(--c-y)` en la misma receta y reclasificar)». Se implementó a medias: el censo CALCULA `derives` y lo imprime en la columna «¿deriva de un público?» de la §3 de cada ficha, pero **el contador nunca lo usó** — esos knobs seguían puntuando como deuda. Medido: **240 privados en 59 componentes** estaban en ese estado, es decir, haciendo exactamente lo que la doctrina prescribe («privados sólo si derivan de públicos») y penalizados por ello. Lo destapó tokenizar `avatar`: sus 17 privados YA leían públicos (`--_avatar-size: var(--avatar-size-md)`), así que no había nada que tokenizar — había que arreglar el instrumento. LA CORRECCIÓN. Segunda pasada tras recoger privados y knobs: un knob que lee `var(--_c-x)` pasa a `public` si ese privado alcanza un público. El cierre es **TRANSITIVO** (`--_a: calc(var(--_b) * .3)` alcanza si `--_b` alcanza) con conjunto de visitados para que un ciclo no lo cuelgue. **Muta-prueba** (obligatoria, memoria `a-guard-that-inspects-nothing-passes`): apuntar `--_avatar-radius` al primitivo crudo baja avatar de 75 % a 72 % y el knob vuelve a `private`; revertirlo lo devuelve a 75 % y deja el árbol idéntico. EFECTO MEDIDO: - Global **50 % → 56 %** · privados 625 → 341 · al 100 % 10 → 14 · <20 % 22 → 20. - `avatar` 36 % → **75 %**, `picker-shell` 76 % → **95 %**, `menu-dial` 25 % → 38 %, y otras 51 fichas con cifras nuevas — **ninguna por cambio de código**. - Los READMEs de `picker-shell` y `menu-dial` se actualizan con la cifra nueva y con la razón, para que nadie crea que el componente cambió. ⚠ **Cualquier cifra anterior a este commit no es comparable con las de ahora.** Queda escrito en el handoff. Y un hueco de F0 que sale al hacerlo, registrado en §13: **el test del SUELO que el plan pedía NO EXISTE** («alcance global ≥ el de hoy y literales ≤ 614 — el número sube o el test falla»). Hoy nada impide que el alcance BAJE entre sesiones: sólo se vería mirando la cifra a mano. Con el censo ya endurecido, es el momento de escribirlo. Gates: `component:audit` 162 PASS / 4 NEEDS-WORK (preexistentes) · `--names` DESVIADAS 0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · el censo compila. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
if (s !== 'private') return false;
return true; // resolved below, per referenced private
});
if (ok && sources.some((s) => s === 'private')) {
// every private this one reads must itself reach a public
ok = (privValues.get(name) ?? []).every((v) => {
const refs = [...v.matchAll(/var\(\s*(--_[a-z0-9-]+)/g)].map((m) => m[1]);
if (refs.length === 0) return !v.includes('var(--') || v.includes(pubNeedle);
return refs.every((r) => derivesFromPublic(r, seen));
});
}
derivesCache.set(name, ok);
return ok;
};
for (const k of knobs) {
if (k.klass !== 'private') continue;
const refs = [...k.value.matchAll(/var\(\s*(--_[a-z0-9-]+)/g)].map((m) => m[1]);
if (refs.length === 0 || !refs.every((r) => derivesFromPublic(r))) continue;
row.private--;
row.public++;
k.klass = 'public';
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const themeable = row.public + row.private + row.global + row.literal;
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
row.knobs = themeable + row.system + row.structural;
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
row.reach = themeable === 0 ? 1 : row.public / themeable;
const declared = new Set(privates.map((p) => p.name));
const privatesFromContract = [...usedPrivates]
.filter((n) => n.startsWith('--_' + dir + '-') && !declared.has(n))
.sort();
return { row, knobs, privates, privatesFromContract, files };
}
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
export function census(only?: string): CensusRow[] {
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
return censusAudit(only).rows;
}
// ─── The debt ledger — R-5.1's per-key ratchet ───────────────────────────────
/**
* The two unreached classes the signature of 2026-08-25 registers key by key.
* `private` is the third and is deliberately NOT here — see the header of
* `theming-census-debt.ts`, which names the hole instead of hiding it.
*/
export type DebtClass = Extract<KnobClass, 'global' | 'literal'>;
const isDebt = (k: Knob): boolean => k.klass === 'global' || k.klass === 'literal';
/**
* The canonical identity of one debt knob: `{class} · {file} · {selector} ·
* {property}`.
*
* LINE-INDEPENDENT on purpose. A ledger keyed by `file:line` would report a
* regression every time a rule moved down three lines, and the noise would
* teach everyone to regenerate the baseline — which is exactly how a ratchet
* dies. The cost of the choice is that a SELECTOR rename reads as one STALE
* plus one new key; that is the ratchet working, not a defect (the debt moved
* and gets re-signed).
*/
export function debtKey(k: Knob): string {
return `${k.klass} · ${k.file} · ${k.selector} · ${k.prop}`;
}
export interface DebtFinding {
component: string;
key: string;
}
export interface DebtAudit {
/** Debt in the CSS that no ledger entry covers — a regression. */
newDebt: DebtFinding[];
/** Ledger entries with no debt behind them — tokenized, annotated or dead. */
stale: DebtFinding[];
/** Ledger entries in scope (the size of the frozen debt). */
registered: number;
/** Per component, for the audit rows. */
byComponent: Map<string, { newDebt: string[]; stale: string[]; registered: number }>;
}
export interface CensusAudit {
rows: CensusRow[];
/** component → its debt keys today, sorted (the multiset the ledger freezes). */
today: Map<string, string[]>;
debt: DebtAudit;
}
/** Occurrence count — the comparison is a MULTISET, so a duplicate is caught. */
function tally(xs: string[]): Map<string, number> {
const m = new Map<string, number>();
for (const x of xs) m.set(x, (m.get(x) ?? 0) + 1);
return m;
}
/**
* ONE pass over the tree that answers both questions the axis asks: the
* classification (`rows`) and the state of the ledger (`debt`). Two passes
* would double the cost for every consumer that needs both — `component-audit`
* needs exactly that for its R-5.1 and R-5.2 rows.
*/
export function censusAudit(only?: string): CensusAudit {
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const rows: CensusRow[] = [];
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
const today = new Map<string, string[]>();
// Sorted so the generated baseline is byte-identical between runs, whatever
// order the filesystem hands the directories back in.
for (const dir of [...readdirSync(ROOT)].sort()) {
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (only && dir !== only) continue;
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const scan = scanComponent(dir);
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
if (!scan) continue;
rows.push(scan.row);
const keys = scan.knobs.filter(isDebt).map(debtKey).sort();
if (keys.length > 0) today.set(dir, keys);
}
const newDebt: DebtFinding[] = [];
const stale: DebtFinding[] = [];
const byComponent = new Map<string, { newDebt: string[]; stale: string[]; registered: number }>();
let registered = 0;
const components = [...new Set([...today.keys(), ...Object.keys(CENSUS_DEBT)])].sort();
for (const component of components) {
if (only && component !== only) continue;
const ledger = CENSUS_DEBT[component] ?? [];
const now = today.get(component) ?? [];
const a = tally(now);
const b = tally(ledger);
const entry = { newDebt: [] as string[], stale: [] as string[], registered: ledger.length };
for (const [key, n] of a)
for (let i = 0; i < n - (b.get(key) ?? 0); i++) entry.newDebt.push(key);
for (const [key, n] of b) for (let i = 0; i < n - (a.get(key) ?? 0); i++) entry.stale.push(key);
entry.newDebt.sort();
entry.stale.sort();
registered += ledger.length;
if (entry.newDebt.length > 0 || entry.stale.length > 0 || entry.registered > 0)
byComponent.set(component, entry);
for (const key of entry.newDebt) newDebt.push({ component, key });
for (const key of entry.stale) stale.push({ component, key });
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
}
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
return { rows, today, debt: { newDebt, stale, registered, byComponent } };
}
/**
* Prettier's own quoting rule (fewest escapes): single quotes unless the
* string carries one and no double quote. Selectors DO carry single quotes
* (`[data-size='sm']`), so getting this wrong means the generated baseline
* fails `npm run lint` — and a baseline that cannot be committed clean is a
* baseline nobody regenerates correctly.
*/
function quote(s: string): string {
if (s.includes("'") && !s.includes('"')) return '"' + s.replace(/\\/g, '\\\\') + '"';
return "'" + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
}
/** The ledger's own source, rewritten below its doctrinal header. */
async function writeDebtBaseline() {
const path = resolve('scripts/theming-census-debt.ts');
const prev = readFileSync(path, 'utf8').replace(/\r\n/g, '\n');
const marker = '\nexport const CENSUS_DEBT: Record<string, string[]> = ';
const at = prev.indexOf(marker);
if (at < 0) throw new Error('theming-census-debt.ts: no `export const CENSUS_DEBT` to replace');
const { today, debt } = censusAudit();
const body = [...today.entries()]
.map(
([component, keys]) =>
`\t${/^[a-z][a-z0-9]*$/.test(component) ? component : quote(component)}: [\n` +
keys.map((k) => `\t\t${quote(k)}`).join(',\n') +
'\n\t]'
)
.join(',\n');
const source = prev.slice(0, at) + marker + '{\n' + body + '\n};\n';
// Formatted by prettier itself, not by hand: the file must land clean under
// `prettier --check .`, and prettier is deterministic, so two generations of
// the same census produce the same bytes.
const prettier = (await import('prettier')) as unknown as {
resolveConfig(p: string): Promise<Record<string, unknown> | null>;
format(src: string, opts: Record<string, unknown>): Promise<string>;
};
const config = (await prettier.resolveConfig(path)) ?? {};
writeFileSync(path, await prettier.format(source, { ...config, filepath: path }), 'utf8');
const total = [...today.values()].reduce((a, k) => a + k.length, 0);
console.log(
`theming-census --debt --write — ${total} debt key(s) across ${today.size} component(s)`
);
console.log(
` delta vs the ledger being replaced: +${debt.newDebt.length} new · -${debt.stale.length} retired`
);
if (debt.newDebt.length > 0 || debt.stale.length > 0)
console.log(' READ THAT DELTA: regenerating wholesale erases the ratchet (see the header).');
}
function reportDebt(only?: string, asJson = false) {
const { debt } = censusAudit(only);
1 month ago
// A check mode that cannot fail is half a guard: red findings set the exit
// code so `--debt` can gate a script or a hook, not only a human reader.
if (debt.newDebt.length > 0 || debt.stale.length > 0) process.exitCode = 1;
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
if (asJson) {
process.stdout.write(
JSON.stringify({ newDebt: debt.newDebt, stale: debt.stale }, null, '\t') + '\n'
);
return;
}
console.log(
`theming-census --debt — ${debt.registered} key(s) registered · ${debt.newDebt.length} new · ${debt.stale.length} stale`
);
if (debt.newDebt.length > 0) {
console.log('\n— NEW DEBT (a literal or a raw global outside the ledger) —');
for (const f of debt.newDebt) console.log(` ${f.component} ${f.key}`);
}
if (debt.stale.length > 0) {
console.log('\n— STALE (registered as debt, no longer debt: delete the line) —');
for (const f of debt.stale) console.log(` ${f.component} ${f.key}`);
}
if (debt.newDebt.length === 0 && debt.stale.length === 0)
console.log(' the ledger matches the CSS exactly.');
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// ─── The proposal — derived from doctrine, never invented ────────────────────
/**
* property → token slot. recipe-contract §1 (dimensional names: logical axes,
* `{part}-height-{size}`, `padding-inline[-{size}]`, `gap`, `radius`,
* `font-size-{size}`, `icon-size-{size}`) + theming §6.7 (colour slots:
* `bg, fg, border, on-bg, on-fg, on-border, hover-bg, on-hover-bg`).
* `null` = the system owns it; a recipe must not mint a token for it.
*/
const PROP_SLOT: Record<string, string | null> = {
background: 'bg',
'background-color': 'bg',
'background-image': 'bg-image',
color: 'fg',
'border-color': 'border',
'border-width': 'border-width',
'border-radius': 'radius',
'box-shadow': 'shadow',
outline: null,
'outline-color': null,
'outline-width': null,
'padding-inline': 'padding-inline',
'padding-block': 'padding-block',
'padding-inline-start': 'padding-inline',
'padding-inline-end': 'padding-inline',
'padding-block-start': 'padding-block',
'padding-block-end': 'padding-block',
gap: 'gap',
'row-gap': 'row-gap',
'column-gap': 'column-gap',
'font-size': 'font-size',
'font-weight': 'font-weight',
'font-family': 'font-family',
'line-height': 'line-height',
'letter-spacing': 'letter-spacing',
'min-block-size': 'height',
'block-size': 'height',
'min-inline-size': 'width',
'inline-size': 'width',
opacity: 'opacity',
fill: 'fill',
stroke: 'stroke',
filter: 'filter',
'backdrop-filter': 'backdrop-filter',
'accent-color': 'accent',
'caret-color': 'caret',
'text-decoration-color': 'underline'
};
/** Physical axes are forbidden as token keys (R-4.4) — they need a decision. */
const PHYSICAL_PROPS = new Set([
'padding',
'padding-top',
'padding-bottom',
'padding-left',
'padding-right',
'border',
'border-top',
'border-bottom',
'border-left',
'border-right',
'border-block',
'border-inline',
'border-block-start',
'border-block-end',
'border-inline-start',
'border-inline-end',
'height',
'min-height',
'width',
'min-width'
]);
const SIZE_KEYS = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
/** Shared layers own an axis; a consumer must NOT mint `--{c}-{axis}` for it. */
const SHARED_LAYERS: Record<string, string[]> = {
'list-surface': [
'color-field',
'combobox',
'command',
'context-menu',
'dropdown-menu',
'fab',
'listbox',
'menu-dial',
'menubar',
'select'
],
'menu-indicator': [
'context-menu',
'dropdown-menu',
'grid-list',
'listbox',
'menubar',
'navigation-menu'
],
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el artefacto de `listbox` (§13), pero total. **`lib/calendar-surface.css`** (hook `data-calendar-surface`): - cuatro coordenadas por talla — `padding`, `control-size`, `day-size`, `font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos por debajo del bundle de control. Esa desviación estaba escondida en cuatro bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un privado con otro nombre; ahora se lee en un sitio. - la FORMA del anillo de evento y de la marca de festivo. **Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y `viewport-placement` componen primitivos del sistema, así que declaran sus públicos en el fichero y no tienen entrada de receta. Ésta no puede: su vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config, así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA y la capa posee sólo lo que una entrada de receta no sabe expresar. **El defecto que la justificaba, medido**: `--calendar-event-shadow` y `--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]` (audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`, `month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo: variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de `commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por `--_calendar-surface-accent`, que cada superficie alimenta con su propio forward de paleta THM-2. **Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker` y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers (`date-picker-calendar`, `-month-view`, `-year-view`, `date-range-picker-calendar`) y un panel portalado no hereda nada del root del picker. Con sólo los cuatro standalone sellando, ambos quedaban con `--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era enganchar la capa a las cuatro identidades de componente: eso viola la regla 1 de capas compartidas, y la respuesta correcta es un sello por wrapper. computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) · year-grid (3.451) · date-picker (464) · date-range-picker (406). `calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO código dan 24 en los mismos nodos y las mismas dos propiedades. Los «missing node» son el propio sello entrando en la clave. **Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los selectores month/year es una moneda al aire — `[data-calendar-month-select][data-button]` (0,2,0) empata con `[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel en un lado ⇒ decisión. De paso, `calendar-select.css` deja de puentear un privado que sólo `[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el fallback, clavado a md fuera cual fuera la talla. **El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en `theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global 43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus consumidores puentean por privados, otra forma, y mueve diez componentes de golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO el catálogo— así que siguen leyendo 0 %. `recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo miraba la receta, y una capa que comparte prefijo con un componente la hacía fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo. eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) · audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) · rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el reformateo en masa que se coló en cuatro README, el test y el censo (churn ajeno, no mío) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'calendar-surface': ['calendar', 'date-picker', 'month-grid', 'range-calendar', 'year-grid'],
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'sliding-indicator': ['radio-group'],
'viewport-placement': ['affix', 'fab', 'menu-dial'],
'spin-field': ['css-field', 'knob', 'number-field'],
'field-segment-state': [
'color-field',
'color-picker',
'date-field',
'date-range-picker',
'time-field'
],
'picker-shell': [
'chronos',
'color-picker',
'date-picker',
'date-range-picker',
'gradient-builder',
'gradient-picker',
'month-grid',
'natural-time-picker',
'palabras',
'time-picker',
'time-range-picker',
'year-grid'
]
};
export interface Proposal {
/** The token key as it goes into `lib/recipes/base.ts` (no `--{c}-` prefix). */
key: string | null;
/** The value — verbatim from today's CSS, so the default cannot move. */
value: string;
/** TSC scope (canon/tsc.md): root · host · color:X · variant:X · size:X. */
scope: string;
/** Why there is no mechanical name (⚠ decisión) — or '' when there is. */
warning: string;
knob: Knob;
}
/** part · size · variant · color · state, read off the selector. */
function readSelector(selector: string, component: string) {
// `:not(:disabled)` asserts the ABSENCE of a state — reading it as the state
// produced `hover-disabled-trigger-bg` for a plain hover. Strip the negations
// before looking for anything.
const sel = selector.replace(/:not\([^)]*\)/g, '');
const partMatch = sel.match(new RegExp('\\[data-' + component + '-([a-z0-9-]+)[\\]=]'));
const part = partMatch ? partMatch[1] : '';
const size = sel.match(/\[data-size='([a-z]+)'\]/)?.[1] ?? '';
const variant = sel.match(/\[data-variant='([a-z-]+)'\]/)?.[1] ?? '';
const color = sel.match(/\[data-color='([a-z-]+)'\]/)?.[1] ?? '';
const state: string[] = [];
if (/:hover/.test(sel)) state.push('hover');
if (/:active|\[data-pressed\]/.test(sel)) state.push('press');
if (/\[data-state='open'\]|\[data-open\]/.test(sel)) state.push('open');
if (/\[data-state='checked'\]|\[data-checked\]/.test(sel)) state.push('on');
if (/\[data-state='selected'\]|\[data-selected\]/.test(sel)) state.push('selected');
if (/\[data-disabled\]|:disabled/.test(sel)) state.push('disabled');
if (/\[data-invalid\]/.test(sel)) state.push('invalid');
if (/:focus-visible|\[data-focused\]/.test(sel)) state.push('focus');
return { part, size, variant, color, state };
}
/** Layout literals are geometry, not a theme knob — flagged, never named. */
const LAYOUT_LITERAL = /^(100%|100vw|100dvh|100vh|1|auto)$/;
function propose(knob: Knob, scan: Scan): Proposal[] {
const component = scan.row.component;
const { part, size, variant, color, state } = readSelector(knob.selector, component);
const slot = PROP_SLOT[knob.prop];
const base = (s: string) => [part, s].filter(Boolean).join('-');
const blocked = (warning: string): Proposal[] => [
{ key: null, value: knob.value, scope: '—', warning, knob }
];
if (slot === null)
return blocked(
'foco: lo posee el sistema (`--focus-ring-*`, theming §32) — no acuñar token propio'
);
if (PHYSICAL_PROPS.has(knob.prop))
return blocked(
'⚠ decisión: `' +
knob.prop +
'` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)'
);
if (!slot) return blocked('⚠ decisión: la propiedad no tiene slot canónico en el vocabulario');
if (/--state-|--focus-ring/.test(knob.value))
return blocked(
'capa de estado / anillo de foco: sistema transversal — no acuñar (recipe-contract §2)'
);
if (LAYOUT_LITERAL.test(knob.value))
return blocked(
'⚠ decisión: `' +
knob.value +
'` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar'
);
// The colour slot vocabulary puts the modifier FIRST (theming §6.7:
// `hover-bg`, `on-bg`); the dimensional one puts the size LAST
// (recipe-contract §1: `padding-inline-{size}`).
const mod = state.filter((s) => s !== 'focus').join('-');
const name = (sizeKey: string) => [variant, mod, base(slot), sizeKey].filter(Boolean).join('-');
const scopeFor = (value: string, sizeKey: string) =>
color
? `color:${color}`
: sizeKey && !size
? `size:${sizeKey}`
: /var\(--[a-z-]*palette-/.test(value)
? 'host'
: 'root';
const note = (value: string, sizeKey: string) => {
if (color)
return 'declaración por color: `declarations[]` con scope `color:' + color + '` (tsc.md)';
if (sizeKey && !SIZE_KEYS.includes(sizeKey)) return '⚠ decisión: talla fuera del canon';
if (/calc\(|min\(|max\(|clamp\(/.test(value))
return '⚠ decisión: el valor es una expresión — el token puede llevar la expresión entera o sólo su término variable';
return '';
};
// A knob reading a PRIVATE resolves through it: the public token takes the
// private's own value, and if the private is redeclared per size, the token
// is minted per size (recipe-contract §1: `{part}-{eje}-{k}`).
if (knob.klass === 'private') {
const ref = knob.value.match(new RegExp('var\\(\\s*(--_' + component + '-[a-z0-9-]+)'))?.[1];
const decls = ref ? scan.privates.filter((p) => p.name === ref) : [];
// The private already reads a public: the knob IS reachable and there is
// nothing to mint. The census counts it as `private` because a regex cannot
// follow the indirection (PLAN-theming §1.3) — that is a measurement limit,
// not debt.
if (decls.length > 0 && decls.every((d) => d.source === 'public'))
return blocked(
'ya alcanzable: el privado `' +
ref +
'` deriva de un público (' +
list([...new Set(decls.map((d) => d.value))].slice(0, 4)) +
') — sin acción; el censo lo cuenta como no alcanzable por el límite del regex'
);
if (decls.length > 0) {
const resolved = decls.map((d) => ({
size: readSelector(d.selector, component).size,
value: d.value
}));
const multi = resolved.filter((r) => r.size).length > 1;
// The size-less declaration in a per-size set IS the default step: the
// catalogue names every one of them (`font-size-{size}`), so leaving it
// bare would mint a name the vocabulary does not have.
if (multi) {
const present = new Set(resolved.map((r) => r.size).filter(Boolean));
const missing = SIZE_KEYS.filter((k) => !present.has(k) && present.size >= 2);
const dflt = missing.includes('md') ? 'md' : '';
for (const r of resolved) if (!r.size && dflt) r.size = dflt;
}
return resolved.map((r) => ({
key: name(multi ? r.size : size),
value: r.value,
scope: scopeFor(r.value, multi ? r.size : ''),
warning:
(note(r.value, multi ? r.size : size)
? note(r.value, multi ? r.size : size) + ' · '
: '') +
'el privado `' +
ref +
'` debe pasar a leer este público (o desaparecer)',
knob
}));
}
return blocked(
'⚠ decisión: el privado que alimenta este knob no se declara en el CSS (viene de `base.ts` o de un estilo inline) — hay que resolverlo antes de nombrarlo'
);
}
return [
{
key: name(size),
value: knob.value,
scope: scopeFor(knob.value, ''),
warning: note(knob.value, size),
knob
}
];
}
// ─── Report ──────────────────────────────────────────────────────────────────
const MARK_START = '<!-- veredicto:start -->';
const MARK_END = '<!-- veredicto:end -->';
const VERDICT_SEED =
MARK_START + '\n\n_(pendiente — lo escribe el autor; se conserva al regenerar)_\n\n' + MARK_END;
const pct = (n: number, d: number) => (d === 0 ? '—' : `${Math.round((100 * n) / d)}%`);
const cell = (s: string) => '`' + s.replace(/\|/g, '\\|') + '`';
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
/** `strct` and not `0%`: the row has no denominator, and that IS the answer. */
const reachOf = (r: CensusRow) =>
STRUCTURAL_COMPONENTS.has(r.component)
? 'strct'
: pct(r.public, r.public + r.private + r.global + r.literal);
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const list = (xs: string[]) => xs.map((k) => '`' + k + '`').join(', ');
/**
* A token BORROWED from another recipe — verified against that recipe's own
* keys in `base.ts`, not by prefix. `--icon-size-sm` is the global icon scale,
* not a token of the `icon` component, and a prefix test called it a loan.
*/
const OWNER_KEYS = new Map<string, Set<string>>();
function borrowedFrom(value: string, self: string): string[] {
const owners = new Set<string>();
for (const m of value.matchAll(/var\(\s*--(_?)([a-z0-9-]+)/g)) {
const priv = m[1] === '_';
const name = m[2];
for (const c of COMPONENT_DIRS) {
if (c === self || !name.startsWith(c + '-')) continue;
if (!OWNER_KEYS.has(c)) OWNER_KEYS.set(c, new Set(contractKeyNames(c)));
const key = (priv ? '_' : '') + name.slice(c.length + 1);
if (OWNER_KEYS.get(c)!.has(key)) owners.add(c);
}
}
return [...owners].sort();
}
function knobTable(rows: Knob[], self: string): string {
if (rows.length === 0) return '_Ninguno._\n';
const out = [
'| # | fichero:línea | selector | propiedad | valor |',
'| ---: | --- | --- | --- | --- |'
];
rows.forEach((k, i) => {
const borrow = borrowedFrom(k.value, self);
const val = cell(k.value) + (borrow.length ? ` ⤴ prestado de ${list(borrow)}` : '');
out.push(
`| ${i + 1} | ${cell(k.file + ':' + k.line)} | ${cell(k.selector)} | ${cell(k.prop)} | ${val} |`
);
});
return out.join('\n') + '\n';
}
function proposalSection(scan: Scan): string {
const c = scan.row.component;
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
// `structural` out with `public` and `system`: proposing a token for a knob
// whose §5 verdict says there is no contract to write would be inventing debt.
const targets = scan.knobs.filter(
(k) => k.klass !== 'public' && k.klass !== 'system' && k.klass !== 'structural'
);
if (targets.length === 0)
return STRUCTURAL_COMPONENTS.has(c)
? '_Ninguna: el componente es **estructural** (§2-bis) — no hay contrato que escribir._\n'
: '_Nada que proponer: no hay knobs fuera de alcance._\n';
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const proposals = targets.flatMap((k) => propose(k, scan));
const minted = new Map<string, { value: Set<string>; scope: string; uses: number }>();
const blocked: Proposal[] = [];
for (const p of proposals) {
if (!p.key) {
blocked.push(p);
continue;
}
const e = minted.get(p.key) ?? { value: new Set<string>(), scope: p.scope, uses: 0 };
e.value.add(p.value);
e.uses++;
minted.set(p.key, e);
}
const out: string[] = [];
out.push(
`### 4.1 Tokens a declarar en \`lib/recipes/base.ts\` (${minted.size})`,
'',
'Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién',
'puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla',
'al final) y theming §6.7 (slots de color, modificador delante). Un token con',
'DOS valores distintos es una colisión de nombre: son dos knobs, o el nombre',
'no distingue lo que debería — se marca `⚠`.',
'',
'| token (`--' + c + '-…`) | scope TSC | valor propuesto | usos |',
'| --- | --- | --- | ---: |'
);
const existing = new Set(contractKeyNames(c));
for (const [key, e] of [...minted.entries()].sort((a, b) => b[1].uses - a[1].uses)) {
const values = [...e.value];
const shown =
values.length === 1 ? cell(values[0]) : '⚠ ' + values.map((v) => cell(v)).join(' / ');
const name = cell(key) + (existing.has(key) ? ' _(ya existe)_' : '');
out.push(`| ${name} | ${cell(e.scope)} | ${shown} | ${e.uses} |`);
}
out.push('');
if (blocked.length > 0) {
const byWarn = new Map<string, Knob[]>();
for (const p of blocked) byWarn.set(p.warning, [...(byWarn.get(p.warning) ?? []), p.knob]);
out.push(`### 4.2 Sin nombre mecánico (${blocked.length})`, '');
for (const [warn, ks] of byWarn) {
out.push(`- **${warn}** — ${ks.length}: ` + list([...new Set(ks.map((k) => k.prop))]) + '.');
}
out.push('');
}
const withWarn = proposals.filter((p) => p.key && p.warning);
if (withWarn.length > 0) {
const byWarn = new Map<string, Set<string>>();
for (const p of withWarn)
byWarn.set(p.warning, (byWarn.get(p.warning) ?? new Set()).add('--' + c + '-' + p.key));
out.push(`### 4.3 Avisos sobre los tokens propuestos (${byWarn.size})`, '');
for (const [warn, keys] of byWarn) out.push(`- **${warn}** — ${list([...keys])}`);
out.push('');
}
return out.join('\n');
}
function doctrineNotes(scan: Scan): string {
const c = scan.row.component;
const notes: string[] = [];
for (const [layer, consumers] of Object.entries(SHARED_LAYERS)) {
if (consumers.includes(c))
notes.push(
`- **Consume la capa compartida \`${layer}\`.** Un eje que la capa posee se consume como \`var(--_x, var(--x))\`; el consumidor **no acuña** \`--${c}-{eje}\` para él — sería un vocabulario paralelo (README de \`eidos/components\`, «Capas compartidas» regla 2).`
);
}
const borrow = new Set<string>();
for (const k of scan.knobs) for (const o of borrowedFrom(k.value, c)) borrow.add(o);
if (borrow.size > 0)
notes.push(
`- **Consume tokens públicos de ${list([...borrow].sort())}.** Un token prestado importa la semántica de su dueño: la corrección no es duplicarlo con prefijo propio, sino la decisión de familia que la auditoría de fase 1 dejó registrada (\`theming-audit.md\` §B, familia calendar).`
);
if (scan.row.knobs === 0)
notes.push(
'- **Sin knobs de apariencia**: el visual vive en un componente compuesto o en una capa compartida. «Declarado y nunca pintado» no es deuda por sí solo (architecture/eidos.md, columna `unused`).'
);
if (scan.row.hasSize)
notes.push(
`- **Tiene eje \`size\`**: los tokens dimensionales van por talla (\`{part}-{eje}-{k}\`) apuntando al bundle \`--size-{k}-*\`, nunca al primitivo crudo (theming §5; el guard \`recipe-css-contract\` prohíbe el primitivo).`
);
return notes.length ? notes.join('\n') + '\n' : '';
}
function privateTable(scan: Scan): string {
if (scan.privates.length === 0) return '_La receta no declara privados propios en su CSS._\n';
const byName = new Map<string, PrivateDecl[]>();
for (const p of scan.privates) byName.set(p.name, [...(byName.get(p.name) ?? []), p]);
const rows = [...byName.entries()].map(([name, decls]) => {
const sources = [...new Set(decls.map((d) => d.source))].sort();
const derives = sources.length === 1 && sources[0] === 'public';
const values = [...new Set(decls.map((d) => d.value))];
const shown = list(values.slice(0, 6)) + (values.length > 6 ? ` …(+${values.length - 6})` : '');
return `| ${cell(name)} | ${decls.length} | ${shown} | ${sources.join(', ')} | ${derives ? '**sí**' : 'no'} |`;
});
return (
[
'| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |',
'| --- | ---: | --- | --- | :-: |',
...rows
].join('\n') + '\n'
);
}
/**
* A component directory with NO `.css` at all: nothing to measure, and that is
* an answer, not a gap. Where its visual actually lives is MEASURED — the
* components it composes, the shared layer it consumes, the foreign recipe that
* styles its attrs — never guessed.
*/
export interface EmptyScan {
component: string;
files: string[];
/** eidos components imported by its wrappers. */
composes: string[];
/** shared layers its wrappers import. */
layers: string[];
/** other components' CSS that selects `[data-{c}…]`. */
styledIn: string[];
contractKeys: string[];
}
const SHARED_LAYER_FILES = [
'list-surface',
'menu-indicator',
'sliding-indicator',
'viewport-placement',
'field-segment-state'
];
export function scanEmpty(dir: string): EmptyScan | null {
const d = join(ROOT, dir);
if (!statSync(d).isDirectory()) return null;
const files = readdirSync(d);
if (files.some((f) => f.endsWith('.css'))) return null;
const src = files
.filter((f) => f.endsWith('.svelte') || f.endsWith('.ts'))
.map((f) => readFileSync(join(d, f), 'utf8'))
.join('\n');
const composes = COMPONENT_DIRS.filter(
(c) => c !== dir && new RegExp('components/' + c + "['/]").test(src)
);
const layers = SHARED_LAYER_FILES.filter((l) => src.includes(l));
const styledIn: string[] = [];
for (const other of COMPONENT_DIRS) {
if (other === dir) continue;
const od = join(ROOT, other);
for (const f of readdirSync(od).filter((x) => x.endsWith('.css'))) {
if (new RegExp('\\[data-' + dir + '[\\]-]').test(readFileSync(join(od, f), 'utf8')))
styledIn.push(`${other}/${f}`);
}
}
return { component: dir, files, composes, layers, styledIn, contractKeys: contractKeyNames(dir) };
}
function emptySheet(scan: EmptyScan, today: string, verdict: string): string {
const c = scan.component;
const why: string[] = [];
if (scan.composes.length > 0)
why.push(
`- **Compone** ${list(scan.composes)}: su apariencia es la de ${scan.composes.length === 1 ? 'ese componente, y se tema en SU ficha' : 'esos componentes, y se tema en SUS fichas'}. Un wrapper no vuelve a pintar lo que compone (component-guide §4).`
);
if (scan.layers.length > 0)
why.push(
`- **Consume la capa compartida** ${list(scan.layers)}: la geometría la posee la capa, y el consumidor no acuña \`--${c}-{eje}\` (README de \`eidos/components\`, «Capas compartidas»).`
);
if (scan.styledIn.length > 0)
why.push(
`- **Sus attrs se pintan desde otra receta**: ${list(scan.styledIn)}. Esos knobs YA están contados en la ficha del anfitrión — contarlos aquí sería contarlos dos veces.`
);
if (why.length === 0)
why.push(
'- **No pinta**: el wrapper aporta estructura, formato o comportamiento, sin superficie propia que un tema pueda mover.'
);
const contract =
scan.contractKeys.length > 0
? `\n> ⚠ **Tiene ${scan.contractKeys.length} clave(s) en \`lib/recipes/base.ts\` sin receta que las consuma** (${list(scan.contractKeys)}). Es la deuda INVERSA de este eje — un token público sin consumidor real; \`recipe-css-contract.test.ts\` falla si un alias público queda huérfano. Disposición: consumirlo o podarlo.\n`
: '';
return `# ${c} — sin receta CSS
> Generado por \`node --import tsx/esm scripts/theming-census.ts --report\`.
> Vista de conjunto: [README](./README.md) · método:
> [\`PLAN-theming.md\`](../../process/PLAN-theming.md) §1.
- **Medido**: ${today} · **Alcance**: **—** (no hay knobs que medir)
- **Ficheros del componente**: ${list(scan.files)} — **ningún \`.css\`**
- **Contrato en \`lib/recipes/base.ts\`**: ${scan.contractKeys.length === 0 ? 'sin entrada' : `${scan.contractKeys.length} clave(s)`}
${contract}
## 1. Dónde vive su visual
${why.join('\n')}
«Declarado y nunca pintado» **no es deuda por sí solo**: el morfo declara la
superficie de COMPORTAMIENTO del componente, no sólo la pintable
(architecture/eidos.md, columna \`unused\`). Un componente sin receta sólo es
deuda si declaró un eje visual que nadie consume — el aviso de arriba lo dice
cuando ocurre.
## 2. Propuesta de corrección
**Ninguna.** Sin receta no hay knob que tokenizar, y acuñar tokens para un
componente que no pinta crearía un vocabulario huérfano. Si algún día pinta,
entra por la puerta normal: receta + tokens en \`base.ts\` + esta ficha con
cifras.
## 3. Veredicto
${verdict}
`;
}
function sheet(scan: Scan, today: string, verdict: string): string {
const { row } = scan;
const c = row.component;
const themeable = row.public + row.private + row.global + row.literal;
const keys = contractKeyNames(c);
const pub = keys.filter((k) => !k.startsWith('_'));
const priv = keys.filter((k) => k.startsWith('_'));
const by = (k: KnobClass) => scan.knobs.filter((x) => x.klass === k);
const contractLine =
keys.length === 0
? '**sin entrada en `base.ts`**'
: `${pub.length} pública(s)${pub.length ? ` — ${list(pub)}` : ''}${priv.length ? ` · ${priv.length} privada(s) forward — ${list(priv)}` : ''}`;
const notes = doctrineNotes(scan);
return `# ${c} — alcance de tema: análisis y propuesta
> Generado por \`node --import tsx/esm scripts/theming-census.ts --report\`.
> Lo **medido** y la **propuesta** se regeneran; el **Veredicto** (§5) se conserva.
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [\`PLAN-theming.md\`](../../process/PLAN-theming.md) §1, §2, §7.
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
- **Medido**: ${today} · **Alcance**: **${reachOf(row)}** — ${
row.structural > 0
? 'ESTRUCTURAL: sin denominador que medir, y eso es la respuesta'
: `${row.public} de ${themeable} knobs por token público`
}
- **Knobs de apariencia**: ${row.knobs} — público ${row.public} · privado ${row.private} · global ${row.global} · literal ${row.literal} · sistema ${row.system} · excepción ${row.exception} · estructural ${row.structural} _(los tres últimos, fuera del ratio)_
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Contrato hoy** (\`lib/recipes/base.ts\`): ${contractLine}
- **Eje \`size\`**: ${row.hasSize ? 'sí' : 'no'} · **ficheros**: ${list(scan.files)}
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (${by('global').length})
${knobTable(by('global'), c)}
### 1.2 A través de un privado (${by('private').length})
${knobTable(by('private'), c)}
### 1.3 Literales (${by('literal').length})
${knobTable(by('literal'), c)}
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 1.4 Excepciones firmadas (${by('exception').length}) — fuera del ratio
Literales que llevan su anotación \`/* literal: <razón> */\` en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
\`component-audit\`. **Una desviación firmada no es deuda** — se listan para que la
razón se lea, no para acuñarlas.
${knobTable(by('exception'), c)}## 2. Sistema transversal (${row.system}) — informativo, fuera del ratio
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
${knobTable(by('system'), c)}${
row.structural > 0
? `
## 2-bis. Estructural (${row.structural}) — fuera del ratio
Su 0 % es **naturaleza, no deuda**: ${STRUCTURAL_COMPONENTS.get(c)}.
La lista firmada vive en \`STRUCTURAL_COMPONENTS\` (\`scripts/theming-census.ts\`) y
el porqué del eje en §13 de \`next-features.md\`. Los knobs se listan para que se
lean, **no** para acuñarlos.
${knobTable(by('structural'), c)}`
: ''
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 3. Privados de la receta — ¿de dónde sale su valor?
${privateTable(scan)}${
scan.privatesFromContract.length > 0
? `\nConsumidos y **no declarados en el CSS** (vienen de \`base.ts\` o de un estilo inline del wrapper): ${list(scan.privatesFromContract)}.\n`
: ''
}
## 4. Propuesta de corrección
${notes ? notes + '\n' : ''}${proposalSection(scan)}
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
- [ ] **Privado que no deriva de un público** — §3 lo marca; el privado debe leer el público o desaparecer.
- [ ] **Velo o acento en el nodo equivocado** (\`archetype: 'item'\` en un envoltorio, un \`background\` en shorthand que mata la capa de estado) — se mide desde el píxel hacia arriba.
- [ ] **Doble animación** al mover un sello a una superficie con animación propia — registro de \`animationstart\`/\`animationend\`.
- [ ] **Diff de computed = 0** en reposo · hover · abierto · disabled · foco, por talla, antes y después.
- [ ] **Centinela por token nuevo**: valor imposible en el root → el nodo lo sigue. Si no, el token miente.
## 5. Veredicto
${verdict}
`;
}
function rootReadme(scans: Scan[], empties: EmptyScan[], today: string, verdict: string): string {
const rows = scans.map((s) => s.row);
const sum = (k: keyof CensusRow) => rows.reduce((a, r) => a + (r[k] as number), 0);
const themeable = sum('public') + sum('private') + sum('global') + sum('literal');
const sorted = [...rows].sort((a, b) => a.reach - b.reach || b.knobs - a.knobs);
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
const structural = rows.filter((r) => STRUCTURAL_COMPONENTS.has(r.component));
// A structural component has no contract BY NATURE: listing it as debt here
// is the same false signal the class exists to kill.
const noContract = rows
.filter((r) => r.contractKeys === 0 && !STRUCTURAL_COMPONENTS.has(r.component))
.map((r) => r.component);
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
const worst = [...rows]
.map((r) => ({ c: r.component, out: r.private + r.global + r.literal, r }))
.sort((a, b) => b.out - a.out)
.slice(0, 20);
return `# Auditoría de alcance de tema — el catálogo entero
> **Generado**, no escrito a mano: \`node --import tsx/esm scripts/theming-census.ts --report\`.
> Este README es la vista de conjunto; **una ficha por componente** al lado, con
> su análisis y su **propuesta de corrección**. Método, clases, protocolo de
> verificación y fases: [\`PLAN-theming.md\`](../../process/PLAN-theming.md).
> La auditoría del SISTEMA de theming (fase 1, cerrada 2026-07-07) es
> [\`theming-audit.md\`](../theming-audit.md); ésta es la deuda de adopción que
> aquélla dejó apuntada en su §5.3-3.
- **Medido**: ${today} · **${rows.length} recetas** con CSS + **${empties.length} componentes sin receta** = ${rows.length + empties.length} fichas, el árbol entero de \`eidos/components/\`
- **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **${pct(sum('public'), themeable)}** — ${sum('public')} de ${themeable} knobs pasan por un token público del componente
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
- **Reparto**: público ${sum('public')} · privado ${sum('private')} · global ${sum('global')} · literal ${sum('literal')} · sistema transversal ${sum('system')} · excepción firmada ${sum('exception')} · estructural ${sum('structural')} _(los tres últimos, fuera del ratio)_
- **Sin token público propio**: ${noContract.length} · **alcance < 20 %**: ${rows.filter((r) => r.reach < 0.2).length} · **alcance 100 %**: ${rows.filter((r) => r.reach === 1 && r.public > 0).length} · **con eje \`size\`**: ${rows.filter((r) => r.hasSize).length} · **estructurales**: ${structural.length}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Cómo se lee
| clase | qué es | ¿lo alcanza un tema del componente? |
| --- | --- | --- |
| \`public\` | \`var(--{c}-…)\`, el contrato del componente | **sí** |
| \`private\` | \`var(--_{c}-…)\`, nombre interno | sólo si el privado deriva de un público (cada ficha lo dice en §3) |
| \`global\` | primitivo del sistema (\`--space-*\`, \`--radius-*\`, \`--color-*\`, bundle \`--size-{k}-*\`…) | sólo moviendo el sistema entero |
| \`literal\` | ni token: \`8px\`, \`1.25\`, \`#fff\` | no — y viola R-2/R-4 |
| \`system\` | sistemas transversales que la receta CONSUME por contrato (capa de estado, anillo de foco, planos de depth, motion, bandas z, opacidad, shape, floating-gap) | sí, **a nivel de sistema**, por diseño (recipe-contract §2) — fuera del ratio |
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| \`exception\` | un literal con su anotación \`/* literal: <razón> */\` en la propia declaración | no hace falta: es la válvula de recipe-contract §3, una desviación ya firmada — fuera del ratio |
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| \`structural\` | **todos** los knobs de un componente cuyo 0 % es su NATURALEZA y no su deuda: no hay contrato que escribir (lista firmada abajo) | no hace falta: no hay superficie que un tema pueda nombrar — fuera del ratio, y su fila lee \`strct\`, no \`0 %\` |
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
**Alcance** = \`public / (public + private + global + literal)\`. \`system\`,
\`exception\` y \`structural\` quedan fuera del denominador: el primero es
tematizable a nivel de sistema por diseño, el segundo es una desviación que el
canon ya aceptó por escrito, y el tercero no tiene nada que un tema pueda nombrar.
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Límites de la medida** (regex sobre el CSS; sobre-reporta, nunca infra-reporta):
una declaración con varios tokens se clasifica por la primera clase que casa
(público → privado → sistema → global); el token público de OTRO componente
cuenta como \`global\` y la ficha lo marca «⤴ prestado»; los valores dentro de
\`@keyframes\` cuentan como cualquier otro. Tres defectos que ningún regex ve —
privado que no deriva, velo en el nodo equivocado, doble animación — van en la
checklist §4.4 de cada ficha.
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
## Estructurales (${structural.length}) — 0 % por naturaleza, no por deuda
Medidos uno a uno el 2026-08-22, con **veredicto §5 escrito** en su ficha:
ninguno tiene contrato que escribir. Mientras el censo los contaba como a un
componente con deuda real, la cifra global mentía por abajo y el gate de F3
(«censo 100 %») era inalcanzable **por construcción** (next-features §13). Sus
knobs se siguen contando y listando —un knob estructural sigue siendo un knob—;
sólo salen del **denominador**, igual que \`system\`. La lista firmada vive en
\`scripts/theming-census.ts\` (\`STRUCTURAL_COMPONENTS\`), con su razón al lado.
${structural.map((r) => `- [\`${r.component}\`](./${r.component}.md) (${r.structural}): ${STRUCTURAL_COMPONENTS.get(r.component)}`).join('\n')}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Qué NO propone una ficha
La propuesta deriva nombres de la doctrina; **no la contradice**. Por eso una
ficha nunca propone acuñar un token para:
- **lo que posee una capa compartida** (\`list-surface\`, \`spin-field\`,
\`picker-shell\`, \`viewport-placement\`, \`field-segment-state\`,
\`menu-indicator\`, \`sliding-indicator\`): un eje = un token público de la capa
+ una ranura privada, consumido \`var(--_x, var(--x))\` — el consumidor no acuña
\`--{c}-{eje}\` (README de \`eidos/components\`, «Capas compartidas» regla 2);
- **el foco** (\`--focus-ring-*\`, theming §32) ni **la capa de estado**
(\`--state-*\`, §38): un knob del sistema, no del componente;
- **un token prestado de otro componente** (la familia \`calendar\` en los
pickers): la corrección es la decisión de familia registrada en
[\`theming-audit.md\`](../theming-audit.md) §B, no un duplicado con prefijo propio;
- **un shorthand o un eje físico** (\`padding\`, \`border\`, \`height\`): primero se
parte en ejes lógicos (recipe-contract §1, R-4.4), y eso es decisión.
## Sin receta CSS (${empties.length})
Ni un \`.css\` en su directorio: **no hay knob que medir**, y eso es una
respuesta, no un hueco. Cada ficha dice —medido, no supuesto— dónde vive su
visual: el componente que compone, la capa compartida que consume, o la receta
ajena que pinta sus attrs (y cuyos knobs ya están contados allí).
${empties.map((e) => `[\`${e.component}\`](./${e.component}.md)`).join(' · ')}
Y el **vocabulario de nombres tiene una desviación medida**: el catálogo habla
mayoritariamente con el modificador al final (\`-bg-hover\`, \`-bg-active\`) mientras
theming §6.7 lo pone delante (\`hover-bg\`). Las fichas proponen la forma
**documentada** y lo dejan anotado: la normalización es **D-TH.6**, sin firmar.
## Los 20 con más knobs fuera del contrato
| # | componente | fuera de alcance | alcance | knobs | contrato |
| ---: | --- | ---: | ---: | ---: | ---: |
${worst.map((w, i) => `| ${i + 1} | [${w.c}](./${w.c}.md) | ${w.out} | ${reachOf(w.r)} | ${w.r.knobs} | ${w.r.contractKeys} |`).join('\n')}
## Sin ningún token público propio (${noContract.length})
Ni una clave pública en \`lib/recipes/base.ts\` — alguno tiene privados forward
(\`_palette-*\`), que no son contrato: un tema no puede nombrarlos.
${noContract.map((c) => `[\`${c}\`](./${c}.md)`).join(' · ')}
## Tabla completa (${rows.length} recetas, por alcance ascendente)
La columna «contrato» cuenta las claves **públicas** del bloque del componente.
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| componente | alcance | knobs | público | privado | global | literal | sistema | estructural | contrato | size |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | :-: |
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
${sorted
.map(
(r) =>
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
`| [${r.component}](./${r.component}.md) | ${reachOf(r)} | ${r.knobs} | ${r.public} | ${r.private} | ${r.global} | ${r.literal} | ${r.system} | ${r.structural} | ${r.contractKeys} | ${r.hasSize ? 'y' : '–'} |`
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
)
.join('\n')}
## Veredicto y decisiones
${verdict}
`;
}
function readVerdict(path: string): string {
if (!existsSync(path)) return VERDICT_SEED;
const prev = readFileSync(path, 'utf8');
const a = prev.indexOf(MARK_START);
const b = prev.indexOf(MARK_END);
if (a < 0 || b <= a) return VERDICT_SEED;
return prev.slice(a, b + MARK_END.length).replace(/\r\n/g, '\n');
}
/**
* HAND-WRITTEN PROSE SURVIVES REGENERATION — anywhere, not just the verdict.
*
* The report rewrites the whole sheet from the census, so an elaboration a
* human added inside §1.1–§1.4 was silently replaced by the template: where
* someone had written «_Ninguno_ — los cuatro que había (…) están cosidos»,
* regeneration left «_Ninguno._». It happened TWICE in one day to the two
* `chat-*` sheets (2026-08-24). Mostly-harmless-occasionally-destructive is
* the worst possible split, because nobody reads the diff.
*
* The contract: wrap hand-written prose in `<!-- mano:start -->` …
* `<!-- mano:end -->` and it is re-inserted after the SAME heading it sat
* under. A block whose heading no longer exists is appended under a
* «rescatado» note rather than dropped — losing prose is never the default.
*/
const HAND_START = '<!-- mano:start -->';
const HAND_END = '<!-- mano:end -->';
type HandBlock = { heading: string; body: string };
function readHandBlocks(path: string): HandBlock[] {
if (!existsSync(path)) return [];
const prev = readFileSync(path, 'utf8').replace(/\r\n/g, '\n');
const out: HandBlock[] = [];
let from = 0;
for (;;) {
const a = prev.indexOf(HAND_START, from);
if (a < 0) break;
const b = prev.indexOf(HAND_END, a);
if (b < 0) break;
const headings = prev.slice(0, a).match(/^#{2,4} .*$/gm);
out.push({
heading: headings ? headings[headings.length - 1] : '',
body: prev.slice(a, b + HAND_END.length)
});
from = b + HAND_END.length;
}
return out;
}
function spliceHandBlocks(sheet: string, blocks: HandBlock[]): string {
if (!blocks.length) return sheet;
let out = sheet;
const orphans: string[] = [];
for (const { heading, body } of blocks) {
if (out.includes(body)) continue;
const i = heading ? out.indexOf(heading + '\n') : -1;
if (i < 0) {
orphans.push(body);
continue;
}
const cut = i + heading.length + 1;
out = out.slice(0, cut) + '\n' + body + '\n' + out.slice(cut);
}
if (orphans.length)
out +=
'\n## Prosa rescatada\n\n' +
'> Estos bloques estaban bajo un encabezado que ya no existe.\n' +
'> Reubicalos o borralos a mano.\n\n' +
orphans.join('\n\n') +
'\n';
return out;
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
function writeReport() {
mkdirSync(REPORT_DIR, { recursive: true });
const today = new Date().toISOString().slice(0, 10);
const scans: Scan[] = [];
const empties: EmptyScan[] = [];
for (const dir of readdirSync(ROOT)) {
const scan = scanComponent(dir);
if (scan) {
scans.push(scan);
continue;
}
const empty = scanEmpty(dir);
if (empty) empties.push(empty);
}
for (const scan of scans) {
const path = join(REPORT_DIR, `${scan.row.component}.md`);
const hand = readHandBlocks(path);
writeFileSync(path, spliceHandBlocks(sheet(scan, today, readVerdict(path)), hand), 'utf8');
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
}
for (const empty of empties) {
const path = join(REPORT_DIR, `${empty.component}.md`);
const handEmpty = readHandBlocks(path);
writeFileSync(path, spliceHandBlocks(emptySheet(empty, today, readVerdict(path)), handEmpty), 'utf8');
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
}
const readmePath = join(REPORT_DIR, 'README.md');
writeFileSync(readmePath, rootReadme(scans, empties, today, readVerdict(readmePath)), 'utf8');
console.log(
`wrote docs/audit/theming/README.md + ${scans.length} recipe sheets + ${empties.length} no-recipe sheets`
);
}
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
// ─── `--names`: the naming grammar (D-TH.6, signed 2026-08-20) ───────────────
/**
* The signed grammar, as data. Three families, three shapes:
*
* role/palette `{part-}?{role|palette}-{COLOR_ROLE_SLOT}` modifier BEHIND
* (`primary-solid-hover`) — canonical BY CONSTRUCTION, the
* slot inventory itself carries `hover`/`active`/`solid-hover`.
* system `--state-*`, `--opacity-*`, `--focus-ring-*` — not a recipe
* key; out of this classifier's reach.
* recipe-level `[variant-]?[modifier-]?[part-]?{slot}[-{size}]` — modifier
* IN FRONT (theming §6.7 r7): "delante lo interactivo, detrás
* lo dimensional y contextual".
*
* The ink slot is `fg`; `color` as a slot is dead (2026-08-20).
*
* WHAT MOVES TO THE FRONT is the INTERACTIVE state, and only it — the closed
* universal vocabulary below (`current` included: it is an ARIA state). A
* qualifier that is contextual (`meter.indicator-bg-below`, `progress`'s
* `loaded`), orientational (`radio-group.gap-vertical`) or modal
* (`sidebar.width-icon`) stays BEHIND: that is the other half of the signed
* sentence, and promoting it would rewrite `--sidebar-width-icon` (the width
* of the icon-collapsed rail) into `--sidebar-icon-width` (the width of an
* icon), which is a different thing. The morfo's declared values are the
* source for VALIDATING that trailing qualifier, never for promoting it.
* Where the author wanted a specific promotion, it is a signed name in
* `NAME_OVERRIDE`, never a rule that generalises silently.
*/
const UNIVERSAL_MODIFIERS = [
'hover',
'active',
'selected',
'disabled',
'checked',
'open',
'focus',
'invalid',
'current'
];
/** Colour-role slots as EMITTED (kebab) — `COLOR_ROLE_SLOTS` + the generated extras. */
const ROLE_SLOT_TAILS = [
'solid-hover',
'surface-hover',
'text-strong',
'hover',
'active',
'text',
'solid',
'surface',
'element',
'track',
'bg2',
'separator',
'border',
'contrast'
];
const ROLES = [
'primary',
'secondary',
'tertiary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss',
'palette'
];
/** Recipe-level variant archetypes that carry their own `-hover`/`-active` pair. */
const VARIANT_TAILS = /(^|-)(solid|surface|soft|outline|ghost|subtle|element)-(hover|active)$/;
/**
* The three false-friend groups the author signed as KEEPING their `color`
* segment (2026-08-20 acta): the system's focus-ring family, colour as a NOUN,
* and `stop-color` as a PART name.
fix(theming)!: avatar - el contrato dice la verdad: 4 claves, cero desviadas Las ultimas CUATRO claves fuera de gramatica de las 4.558 publicas del catalogo, resueltas cada una por lo que ES (no eran cuatro iguales): - `badge-color-custom-contrast` -> `badge-fg-custom-contrast`: la unica TINTA real y alcanzable. Nombre derivado DE la gramatica D-TH.6 (la ranura se sustituye EN SU SITIO; `custom` no es interactivo ni rol, va detras) con el precedente firmado scrim-fg-over-dark - y es la salida literal del clasificador que consume R-5.3. Renombre a mano (el codemod ABORTA sobre la IIFE de avatar - deuda registrada en 13): value-preserving, verificado con sonda dirigida (la clave vieja movia antes y no despues; la nueva al reves). - `ring-color-custom` y `badge-color-custom` RETIRADAS del contrato publico: canales de valor INALCANZABLES (puerta data-*-custom y escritura inline nacen de la misma expresion - mentiras bajo R-5.4). Mecanismo: canal privado de envoltorio `--_avatar-*-color-custom` + respaldo currentColor (el patron de las 5 privadas-de-envoltorio del parque, 5/5 sin declaracion en receta), NO borrado (el CSS las lee) NI privada plana (cero precedente). Ley 8.bis re-aplicada: cero lectores fuera de avatar, re-barrido. - `group-carve-color` EXENTA en NAME_EXEMPT con razon medida: alimenta box-shadow (geometria del carve, no tinta; alcanza en 59 instancias) - `fg` seria gramatica correcta con semantica peor. Medido: sonda 7.4 0/576 - pixel visible IDENTICO (anillo ambar, insignia violeta, tinta white) e instancia imposible identica (currentColor preserva el default retirado) - Tokens 88 -> 86 EN VIVO con el nombre nuevo (captura revisada por el supervisor) - censo --names: DESVIADAS 0 EN 0 COMPONENTES (4.556 claves) - centinela exit 0 - lint 0 - docs-check 0/0 - vitest 442+1 ajeno - check 0 atribuibles. Hallazgo de instrumento NUEVO, adjudicado y registrado en 13: el chooser del centinela elige el valor por NOMBRE y no conoce `fg` MEDIAL (su prueba de tinta es `fg$`): las 5 claves que la propia D-TH.6 fabrica (badge-fg-custom- contrast, bubble-fg-in/-out, scrim-fg-over-dark/-light) reciben 1234px, invalido para color - y el veredicto depende de si el respaldo coincide por azar (bubble-fg-* leen vivas POR ACCIDENTE). Arreglarlo obliga a re-verificar el ledger - firma de instrumento aparte. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
*
* The fourth (2026-08-25 acta) is of a different kind: not a false friend but a
* measured one. The classifier decides by the FORM of the name; `group-carve-color`
* is the one key whose DESTINATION contradicts it.
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
*/
const NAME_EXEMPT: Array<{ re: RegExp; why: string }> = [
{ re: /focus-ring-color$/, why: 'espeja la familia del SISTEMA --focus-ring-*' },
{ re: /^orb-color-/, why: 'color como sustantivo del orbe, no slot' },
fix(theming)!: avatar - el contrato dice la verdad: 4 claves, cero desviadas Las ultimas CUATRO claves fuera de gramatica de las 4.558 publicas del catalogo, resueltas cada una por lo que ES (no eran cuatro iguales): - `badge-color-custom-contrast` -> `badge-fg-custom-contrast`: la unica TINTA real y alcanzable. Nombre derivado DE la gramatica D-TH.6 (la ranura se sustituye EN SU SITIO; `custom` no es interactivo ni rol, va detras) con el precedente firmado scrim-fg-over-dark - y es la salida literal del clasificador que consume R-5.3. Renombre a mano (el codemod ABORTA sobre la IIFE de avatar - deuda registrada en 13): value-preserving, verificado con sonda dirigida (la clave vieja movia antes y no despues; la nueva al reves). - `ring-color-custom` y `badge-color-custom` RETIRADAS del contrato publico: canales de valor INALCANZABLES (puerta data-*-custom y escritura inline nacen de la misma expresion - mentiras bajo R-5.4). Mecanismo: canal privado de envoltorio `--_avatar-*-color-custom` + respaldo currentColor (el patron de las 5 privadas-de-envoltorio del parque, 5/5 sin declaracion en receta), NO borrado (el CSS las lee) NI privada plana (cero precedente). Ley 8.bis re-aplicada: cero lectores fuera de avatar, re-barrido. - `group-carve-color` EXENTA en NAME_EXEMPT con razon medida: alimenta box-shadow (geometria del carve, no tinta; alcanza en 59 instancias) - `fg` seria gramatica correcta con semantica peor. Medido: sonda 7.4 0/576 - pixel visible IDENTICO (anillo ambar, insignia violeta, tinta white) e instancia imposible identica (currentColor preserva el default retirado) - Tokens 88 -> 86 EN VIVO con el nombre nuevo (captura revisada por el supervisor) - censo --names: DESVIADAS 0 EN 0 COMPONENTES (4.556 claves) - centinela exit 0 - lint 0 - docs-check 0/0 - vitest 442+1 ajeno - check 0 atribuibles. Hallazgo de instrumento NUEVO, adjudicado y registrado en 13: el chooser del centinela elige el valor por NOMBRE y no conoce `fg` MEDIAL (su prueba de tinta es `fg$`): las 5 claves que la propia D-TH.6 fabrica (badge-fg-custom- contrast, bubble-fg-in/-out, scrim-fg-over-dark/-light) reciben 1234px, invalido para color - y el veredicto depende de si el respaldo coincide por azar (bubble-fg-* leen vivas POR ACCIDENTE). Arreglarlo obliga a re-verificar el ledger - firma de instrumento aparte. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
{ re: /^stop-color(-|$)/, why: 'stop-color ES la parte (gradient-builder-stop-color.svelte)' },
{
re: /^group-carve-color$/,
why: 'alimenta box-shadow (la GEOMETRÍA del carve, no tinta): avatar.css `[data-carve-out] { box-shadow: 0 0 0 var(--avatar-group-carve-width) var(--avatar-group-carve-color) }`. Alcanza — medido en las 59 instancias de /uix/components/avatar-group (oklch(0.9911 0 0) → rgb(1,2,3), restaurado). `group-carve-fg` sería gramática correcta y semántica PEOR: nombraría tinta lo que es el filo que recorta la silueta contra el avatar anterior'
}
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
];
/**
* Destinations the author decided by NAME in the 2026-08-20 acta, where the
* generic grammar cannot deduce them. Each one is a reading of the component,
* not a rule: `on-dark` reads as the canonical `on-` accent prefix and had to
* move away from it; `played`/`buffered` are waveform REGIONS (pseudo-parts),
* so they lead like a part; `inactive` IS rating-group's resting state, and a
* resting state carries no modifier.
*/
const NAME_OVERRIDE: Record<string, string> = {
'background.scrim-color-on-dark': 'scrim-fg-over-dark',
'background.scrim-color-on-light': 'scrim-fg-over-light',
'waveform.color-played': 'played-fg',
'waveform.color-buffered': 'buffered-fg',
'rating-group.item-color-inactive': 'item-fg',
// Estados de ITEM que el autor nombró uno a uno: son interactivos/de estatus
// y van delante, pero no salen de una regla — salen de su firma.
'rating-group.item-color-active': 'active-item-fg',
'rating-group.item-color-partial': 'partial-item-fg',
'chat-message.status-color-read': 'read-status-fg',
'chat-message.status-color-failed': 'failed-status-fg'
};
export type NameClass =
| 'ok' // already speaks the grammar
| 'role-slot' // canonical by construction — never touch
| 'exempt' // signed false friend
| 'state-layer' // neutral hover: MIGRATES (firma 3), not renamed
| 'ink' // `color` as slot → `fg`
| 'modifier' // modifier behind → in front
| 'both';
export interface NameFinding {
component: string;
key: string;
klass: NameClass;
to?: string;
why?: string;
}
/** Values every `data-*` axis of the component's morfo declares (lowercased). */
async function morfoModifiers(component: string): Promise<Set<string>> {
const out = new Set<string>();
const path = resolve(`src/uix/morfo/components/${component}.ts`);
if (!existsSync(path)) return out;
try {
const mod = (await import(`../src/uix/morfo/components/${component}`)) as Record<
string,
unknown
>;
const key = `${component.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())}Morfo`;
const morfo = mod[key];
if (!morfo) return out;
const compiled = compileMorfo(morfo as Morfo);
for (const contracts of compiled.contracts.dataAttrsByPart.values())
for (const c of contracts) for (const v of c.values ?? []) out.add(v.toLowerCase());
} catch {
/* a morfo that will not import is the eidos-lint's business, not the census's */
}
return out;
}
fix(eidos): la capa de estado es un VELO — 38 hovers que no lo eran, al idioma El codemod de ayer apartó 47 knobs «en cola de migración a la capa de estado». Al empezar esa migración se ve que sólo SEIS lo estaban: el velo de §38 es `background-image: linear-gradient(var(--state-hover), var(--state-hover))` y no alcanza nada más. De los otros 41, veintidós mueven un BORDE y diecinueve mueven la TINTA — ninguno es la capa de estado por muy neutro que sea su valor. Eran deuda de nombre, y yo los había excluido del renombrado. El clasificador tenía media prueba: miraba el VALOR (¿neutro o con valencia?) y no la PROPIEDAD. Ahora exige las dos, siguiendo un salto por privado (`--_switch-track-bg-hover`) y entre componentes (la familia calendar presta `--calendar-control-*` a month-grid, range-calendar y year-grid, y por eso esos cuatro no aparecían consumidos en su propia receta). renombres 38 en 22 componentes · 113 referencias en 30 ficheros cola real 6 (checkbox · date-range-picker ×2 · scroll-area · splitter · switch) — los únicos que pintan fondo censo 162 · 5.203 · 1.841 (37 %) · 56 — IDÉNTICO --names 38 → 0 desviadas diff generated 38 renombres 1:1 · 0 cambios de valor computed field · tabs contra la línea base ORIGINAL (anterior a las dos pasadas): 4.031 valores, 16 estados, 0 diffs huérfanos 0 · formato sin regresión · audit 163 PASS · rtl 0 · docs 0 · suite 434 pasan (1 rojo conocido) · check 0 errores en ficheros tocados La muta-prueba se reescribió contra el catálogo vivo — asertaba nombres que el codemod ya había renombrado — y gana los casos que faltaban: un knob de borde neutro es `modifier`, uno de tinta neutro es `both`, y sólo los de fondo son `state-layer`. Queda una pregunta que §38 no contesta y que NO he decidido: si un hover de borde o de tinta POR COMPONENTE debe existir siquiera. R-4.3 guarda `background*` en `:hover`, nada más. Ahora al menos se llaman como deben mientras se decide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/** Every component recipe CSS, read once — a token may be consumed by a SIBLING. */
let cssIndex: Array<{ component: string; text: string }> | null = null;
function allComponentCss(): Array<{ component: string; text: string }> {
if (cssIndex) return cssIndex;
cssIndex = [];
for (const dir of readdirSync(ROOT)) {
const path = join(ROOT, dir, `${dir}.css`);
if (!existsSync(path)) continue;
cssIndex.push({ component: dir, text: readFileSync(path, 'utf8').replace(/\r\n/g, '\n') });
}
return cssIndex;
}
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
/**
fix(eidos): la capa de estado es un VELO — 38 hovers que no lo eran, al idioma El codemod de ayer apartó 47 knobs «en cola de migración a la capa de estado». Al empezar esa migración se ve que sólo SEIS lo estaban: el velo de §38 es `background-image: linear-gradient(var(--state-hover), var(--state-hover))` y no alcanza nada más. De los otros 41, veintidós mueven un BORDE y diecinueve mueven la TINTA — ninguno es la capa de estado por muy neutro que sea su valor. Eran deuda de nombre, y yo los había excluido del renombrado. El clasificador tenía media prueba: miraba el VALOR (¿neutro o con valencia?) y no la PROPIEDAD. Ahora exige las dos, siguiendo un salto por privado (`--_switch-track-bg-hover`) y entre componentes (la familia calendar presta `--calendar-control-*` a month-grid, range-calendar y year-grid, y por eso esos cuatro no aparecían consumidos en su propia receta). renombres 38 en 22 componentes · 113 referencias en 30 ficheros cola real 6 (checkbox · date-range-picker ×2 · scroll-area · splitter · switch) — los únicos que pintan fondo censo 162 · 5.203 · 1.841 (37 %) · 56 — IDÉNTICO --names 38 → 0 desviadas diff generated 38 renombres 1:1 · 0 cambios de valor computed field · tabs contra la línea base ORIGINAL (anterior a las dos pasadas): 4.031 valores, 16 estados, 0 diffs huérfanos 0 · formato sin regresión · audit 163 PASS · rtl 0 · docs 0 · suite 434 pasan (1 rojo conocido) · check 0 errores en ficheros tocados La muta-prueba se reescribió contra el catálogo vivo — asertaba nombres que el codemod ya había renombrado — y gana los casos que faltaban: un knob de borde neutro es `modifier`, uno de tinta neutro es `both`, y sólo los de fondo son `state-layer`. Queda una pregunta que §38 no contesta y que NO he decidido: si un hover de borde o de tinta POR COMPONENTE debe existir siquiera. R-4.3 guarda `background*` en `:hover`, nada más. Ahora al menos se llaman como deben mientras se decide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* The CSS properties a token feeds, following ONE hop through a private
* (`--_switch-track-bg-hover: var(--switch-track-bg-off-hover)`) and across
* components (the calendar family lends `--calendar-control-*` to month-grid,
* range-calendar and year-grid).
*/
function propertiesFed(token: string): string[] {
const props: string[] = [];
const privates: string[] = [];
for (const { text } of allComponentCss()) {
if (!text.includes(token)) continue;
for (const line of text.split('\n')) {
if (!line.includes(token)) continue;
const m = line.match(/^\s*(--[a-z0-9_-]+|[a-z-]+)\s*:/);
if (!m) continue;
if (m[1].startsWith('--')) privates.push(m[1]);
else props.push(m[1]);
}
}
for (const priv of privates)
for (const { text } of allComponentCss()) {
if (!text.includes(priv)) continue;
for (const line of text.split('\n')) {
if (!line.includes(priv)) continue;
const m = line.match(/^\s*([a-z-]+)\s*:/);
if (m) props.push(m[1]);
}
}
return props;
}
/**
* A knob heads for the state-layer migration (§38 + R-4.3) only when it is
* BOTH neutral in value AND painting a BACKGROUND — the veil of §38 is
* `background-image: linear-gradient(var(--state-hover), var(--state-hover))`
* and it reaches nothing else.
*
* Measured 2026-08-20, and it corrected this classifier: of the 47 knobs the
* value test alone had queued, only SIX paint a background. Twenty-two move a
* BORDER and nineteen move the INK — neither is the state layer, whatever
* their value, so they are ordinary naming debt and the codemod renames them.
* Whether a per-component border/ink hover should exist AT ALL is a separate
* question that §38 does not settle (R-4.3 guards `background*` only).
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
*
fix(eidos): la capa de estado es un VELO — 38 hovers que no lo eran, al idioma El codemod de ayer apartó 47 knobs «en cola de migración a la capa de estado». Al empezar esa migración se ve que sólo SEIS lo estaban: el velo de §38 es `background-image: linear-gradient(var(--state-hover), var(--state-hover))` y no alcanza nada más. De los otros 41, veintidós mueven un BORDE y diecinueve mueven la TINTA — ninguno es la capa de estado por muy neutro que sea su valor. Eran deuda de nombre, y yo los había excluido del renombrado. El clasificador tenía media prueba: miraba el VALOR (¿neutro o con valencia?) y no la PROPIEDAD. Ahora exige las dos, siguiendo un salto por privado (`--_switch-track-bg-hover`) y entre componentes (la familia calendar presta `--calendar-control-*` a month-grid, range-calendar y year-grid, y por eso esos cuatro no aparecían consumidos en su propia receta). renombres 38 en 22 componentes · 113 referencias en 30 ficheros cola real 6 (checkbox · date-range-picker ×2 · scroll-area · splitter · switch) — los únicos que pintan fondo censo 162 · 5.203 · 1.841 (37 %) · 56 — IDÉNTICO --names 38 → 0 desviadas diff generated 38 renombres 1:1 · 0 cambios de valor computed field · tabs contra la línea base ORIGINAL (anterior a las dos pasadas): 4.031 valores, 16 estados, 0 diffs huérfanos 0 · formato sin regresión · audit 163 PASS · rtl 0 · docs 0 · suite 434 pasan (1 rojo conocido) · check 0 errores en ficheros tocados La muta-prueba se reescribió contra el catálogo vivo — asertaba nombres que el codemod ya había renombrado — y gana los casos que faltaban: un knob de borde neutro es `modifier`, uno de tinta neutro es `both`, y sólo los de fondo son `state-layer`. Queda una pregunta que §38 no contesta y que NO he decidido: si un hover de borde o de tinta POR COMPONENTE debe existir siquiera. R-4.3 guarda `background*` en `:hover`, nada más. Ahora al menos se llaman como deben mientras se decide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
* The value test still runs first: a VALENCED hover is the recipe's palette
* swap (recipe-contract §2), legitimate and renamed like any other key —
* `dropdown-menu.item-bg-hover` reads `var(--color-primary-element)` and only
* its NAME looks neutral.
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
*/
function isNeutralHover(component: string, key: string): boolean {
if (!/(^|-)hover(-|$)/.test(key)) return false;
if (VARIANT_TAILS.test(key)) return false;
const block = recipeBlock(component);
if (block === null) return false;
const decl = block.match(new RegExp("^\\t\\t'?" + key + "'?\\s*:\\s*(.+)$", 'm'));
const value = decl?.[1] ?? '';
const valenced = new RegExp('var\\(--color-(' + ROLES.join('|') + ')-').test(value);
if (valenced) return false;
fix(eidos): la capa de estado es un VELO — 38 hovers que no lo eran, al idioma El codemod de ayer apartó 47 knobs «en cola de migración a la capa de estado». Al empezar esa migración se ve que sólo SEIS lo estaban: el velo de §38 es `background-image: linear-gradient(var(--state-hover), var(--state-hover))` y no alcanza nada más. De los otros 41, veintidós mueven un BORDE y diecinueve mueven la TINTA — ninguno es la capa de estado por muy neutro que sea su valor. Eran deuda de nombre, y yo los había excluido del renombrado. El clasificador tenía media prueba: miraba el VALOR (¿neutro o con valencia?) y no la PROPIEDAD. Ahora exige las dos, siguiendo un salto por privado (`--_switch-track-bg-hover`) y entre componentes (la familia calendar presta `--calendar-control-*` a month-grid, range-calendar y year-grid, y por eso esos cuatro no aparecían consumidos en su propia receta). renombres 38 en 22 componentes · 113 referencias en 30 ficheros cola real 6 (checkbox · date-range-picker ×2 · scroll-area · splitter · switch) — los únicos que pintan fondo censo 162 · 5.203 · 1.841 (37 %) · 56 — IDÉNTICO --names 38 → 0 desviadas diff generated 38 renombres 1:1 · 0 cambios de valor computed field · tabs contra la línea base ORIGINAL (anterior a las dos pasadas): 4.031 valores, 16 estados, 0 diffs huérfanos 0 · formato sin regresión · audit 163 PASS · rtl 0 · docs 0 · suite 434 pasan (1 rojo conocido) · check 0 errores en ficheros tocados La muta-prueba se reescribió contra el catálogo vivo — asertaba nombres que el codemod ya había renombrado — y gana los casos que faltaban: un knob de borde neutro es `modifier`, uno de tinta neutro es `both`, y sólo los de fondo son `state-layer`. Queda una pregunta que §38 no contesta y que NO he decidido: si un hover de borde o de tinta POR COMPONENTE debe existir siquiera. R-4.3 guarda `background*` en `:hover`, nada más. Ahora al menos se llaman como deben mientras se decide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (!/var\(--color-(surface|content|border|neutral)[-)]/.test(value)) return false;
return propertiesFed(`--${component}-${key}`).some((p) => /^background/.test(p));
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
}
/** Classify one recipe key against the signed grammar. */
function classifyName(component: string, key: string, modifiers: Set<string>): NameFinding {
const at = (klass: NameClass, to?: string, why?: string): NameFinding => ({
component,
key,
klass,
to,
why
});
const override = NAME_OVERRIDE[component + '.' + key];
if (override) return at(/(^|-)color(-|$)/.test(key) ? 'ink' : 'modifier', override, 'acta');
const exempt = NAME_EXEMPT.find((e) => e.re.test(key));
if (exempt) return at('exempt', undefined, exempt.why);
const roleSlot =
ROLES.some((r) => new RegExp('(^|-)' + r + '-(' + ROLE_SLOT_TAILS.join('|') + ')$').test(key)) ||
VARIANT_TAILS.test(key);
if (roleSlot) return at('role-slot');
if (isNeutralHover(component, key)) return at('state-layer', undefined, 'migra a --state-hover');
// Peel the trailing INTERACTIVE state only. A contextual qualifier keeps its
// place (see the header): `modifiers` validates it, it never promotes it.
const valid = new Set(UNIVERSAL_MODIFIERS);
const mods: string[] = [];
let base = key;
for (;;) {
const hit = [...valid].find((m) => base.endsWith('-' + m) && base.length > m.length + 1);
if (!hit) break;
mods.unshift(hit);
base = base.slice(0, -(hit.length + 1));
}
const ink = /(^|-)color(-|$)/.test(base) && !/^color-scheme/.test(base);
if (!ink && mods.length === 0) return at('ok');
// `color` as the ink slot → `fg`; `color` as a medial segment keeps its
// neighbours (`status-color` → `status-fg`).
if (ink) base = base === 'color' ? 'fg' : base.replace(/(^|-)color(-|$)/, (_, a, b) => a + 'fg' + b);
const to = mods.length ? mods.join('-') + '-' + base : base;
if (to === key) return at('ok');
return at(ink && mods.length ? 'both' : ink ? 'ink' : 'modifier', to);
}
export async function names(only?: string): Promise<NameFinding[]> {
const out: NameFinding[] = [];
for (const dir of readdirSync(ROOT)) {
if (only && dir !== only) continue;
if (recipeBlock(dir) === null) continue;
const modifiers = await morfoModifiers(dir);
for (const key of contractKeyNames(dir)) {
if (key.startsWith('_')) continue;
out.push(classifyName(dir, key, modifiers));
}
}
return out;
}
async function reportNames(only?: string, asJson = false) {
const findings = await names(only);
if (asJson) {
process.stdout.write(JSON.stringify(findings, null, '\t') + '\n');
return;
}
const of = (k: NameClass) => findings.filter((f) => f.klass === k);
const deviated = [...of('ink'), ...of('modifier'), ...of('both')];
const comps = new Set(deviated.map((f) => f.component));
console.log(`theming-census --names — ${findings.length} claves públicas`);
console.log(
` DESVIADAS ${deviated.length} en ${comps.size} componentes · ink ${of('ink').length} · modificador ${of('modifier').length} · ambas ${of('both').length}`
);
console.log(
` conformes ${of('ok').length} · role-slot canónicas ${of('role-slot').length} · exentas ${of('exempt').length} · capa de estado ${of('state-layer').length}`
);
const per = new Map<string, number>();
for (const f of deviated) per.set(f.component, (per.get(f.component) ?? 0) + 1);
console.log(
' peores: ' +
[...per.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([c, n]) => `${c}:${n}`)
.join(' ')
);
console.log('\n— DESVIADAS (el codemod las renombra) —');
for (const f of deviated) console.log(` ${f.component}.${f.key} → ${f.to}`);
console.log('\n— CAPA DE ESTADO (migran por la firma 3, NO se renombran) —');
for (const f of of('state-layer')) console.log(` ${f.component}.${f.key}`);
console.log('\n— EXENTAS (firmadas) —');
for (const f of of('exempt')) console.log(` ${f.component}.${f.key} (${f.why})`);
}
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
function main() {
const args = process.argv.slice(2);
const only = args.includes('--only') ? args[args.indexOf('--only') + 1] : undefined;
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y sus 793 referencias en 101 ficheros; no toca un solo valor. claves renombradas 269 en 66 componentes referencias 793 en 101 ficheros censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56 sin contrato — IDÉNTICO, como debe ser un rename --names antes/después 269 desviadas → 0 Qué NO entra, y por qué: 47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador detrás POR CONSTRUCCIÓN (`primary-solid-hover`) 13 exentas firmadas `--focus-ring-*` es familia del sistema; el color de `aura` es un sustantivo; `stop-color` ES una parte de gradient-builder 47 hovers neutros por VALOR: migran a la capa de estado (§38 + R-4.3), no se renombran — firma 3 6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background: reescribir un registro fechado lo vuelve mentira El clasificador vive en el censo (`--names`), no en un script suelto, para que el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba (`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo promovía al frente CUALQUIER valor declarado por el morfo, y así `--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en `--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo promociona el vocabulario interactivo cerrado, y lo contextual —`below`, `loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de regresión están en la muta-prueba. También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`: es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una migración que no le toca; se clasifica por el VALOR. Verificación (§7.4, artefacto por paso): diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados reapuntados a los nombres nuevos (`__names-verify-diff`) computed tag-group · field · tabs: 6.467 valores en 23 estados, 0 diffs — y comprobado en el navegador que sirve el CSS nuevo, para que ese 0 no sea el de una copia cacheada huérfanos 0 en código suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`). `recipe-css-contract` verde: es el guard que caza el `var()` sin fallback a un nombre que ya no se emite, o sea el fallo exacto de un rename a medias component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto check 0 errores en ficheros tocados (los 73 globales son de otras sesiones de la rama; se atribuye por fichero) rtl:check 0 · docs:check 0 formato el renombrado no alarga ninguna línea: ningún fichero tiene más líneas de 100 chars que antes, así que no se pasa prettier — hacerlo reformateaba 300 ficheros de deriva ajena Entra aquí la corrección del repaso de los 7 ya hechos: `gradient-builder.checker-color` → `checker-fg` (el damero de transparencia; ahí `color` era slot). Sus `stop-color-*` no se tocan. Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran antes (firma 3, ya firmada), o el guard necesita una exención greppable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (args.includes('--names')) {
void reportNames(only, args.includes('--json'));
return;
}
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
if (args.includes('--debt')) {
if (args.includes('--write')) void writeDebtBaseline();
else reportDebt(only, args.includes('--json'));
return;
}
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (args.includes('--report')) {
writeReport();
return;
}
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
const { rows, debt } = censusAudit(only);
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
if (args.includes('--json')) {
process.stdout.write(JSON.stringify(rows, null, '\t') + '\n');
return;
}
const sum = (k: keyof CensusRow) => rows.reduce((s, r) => s + (r[k] as number), 0);
const themeable = sum('public') + sum('private') + sum('global') + sum('literal');
console.log(`theming-census — ${rows.length} component recipe(s)`);
console.log(
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
` appearance knobs ${sum('knobs')} · public ${sum('public')} (${pct(sum('public'), themeable)} reach) · private ${sum('private')} · global ${sum('global')} · literal ${sum('literal')} · system ${sum('system')} · exception ${sum('exception')} · structural ${sum('structural')}`
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
);
console.log(
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
` no contract entry: ${rows.filter((r) => r.contractKeys === 0 && !STRUCTURAL_COMPONENTS.has(r.component)).length} · reach < 20%: ${rows.filter((r) => r.reach < 0.2).length} · reach = 100%: ${rows.filter((r) => r.reach === 1 && r.public > 0).length} · with data-size: ${rows.filter((r) => r.hasSize).length} · structural: ${rows.filter((r) => STRUCTURAL_COMPONENTS.has(r.component)).length}`
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
);
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
// The ratchet, in the same breath as the totals it used to hide behind:
// five regressions under five fixes moved neither `literal` nor `global`.
console.log(
` debt ledger: ${debt.registered} registered · ${debt.newDebt.length} NEW · ${debt.stale.length} STALE (detail: --debt)`
);
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
console.log('');
const sorted = [...rows].sort((a, b) => a.reach - b.reach || b.knobs - a.knobs);
console.log(
'component'.padEnd(24) +
'reach'.padStart(6) +
'knobs'.padStart(7) +
'public'.padStart(8) +
'private'.padStart(9) +
'global'.padStart(8) +
'literal'.padStart(9) +
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'excep'.padStart(7) +
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'system'.padStart(8) +
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
'strct'.padStart(7) +
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'contract'.padStart(10) +
' size'
);
for (const r of sorted) {
console.log(
r.component.padEnd(24) +
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
reachOf(r).padStart(6) +
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
String(r.knobs).padStart(7) +
String(r.public).padStart(8) +
String(r.private).padStart(9) +
String(r.global).padStart(8) +
String(r.literal).padStart(9) +
tooling(theming): el censo aprende a leer la válvula del canon — clase `exception` Global 58 % → **59 %** sin tocar una línea de CSS: 54 knobs que eran deuda resultan ser desviaciones que el canon ya había firmado. `recipe-contract` §3 dice que una desviación anotada `/* literal: <razón> */` EN SU PROPIA DECLARACIÓN es válida, y `component-audit` la honra desde siempre (`declarationWindow` + `/literal:/i` en R-2.1, R-2.6 y R-2.7). El censo era el único instrumento que no: su `strip()` blanquea los comentarios ANTES de clasificar, así que las **81 anotaciones del catálogo** contaban como literales sin excusa. Dos instrumentos, un canon, dos respuestas — y la que mandaba era la del que no sabía leerlo. Ahora un literal anotado cae en la clase **`exception`, fuera del ratio** como `system`, y por la misma razón: no es que un tema no lo alcance, es que el canon ya aceptó por escrito que ahí no va un token. **54 knobs en 12 componentes**: `prose` 21 (su escala editorial em-relativa, que su README documenta como decisión — «No 5× duplicated scale»), `proof-of-human` 16 (la escena de tono fijo), `palabras` 3, `timeline` 2 (73 % → **77 %**), `result` 1 (86 % → **90 %**), y siete más con una cada uno. Detalles que costaron medición: - **La anotación vale para la DECLARACIÓN entera**, no sólo para su primera línea: un valor partido en varias líneas lleva la nota al final. Se comprueban todas las líneas que abarca la declaración. - **`strip()` blanquea en el sitio** (sustituye por espacios, conserva los saltos), así que el cuerpo despojado y el crudo quedan alineados línea a línea y basta con guardar el crudo al lado. - Muta-prueba: retirar una anotación de `prose` mueve el knob de `exception` a `literal` (21 → 20) y el conteo vuelve al restaurarla. Efecto colateral bueno: cada ficha gana una **§1.4 «Excepciones firmadas»** que las LISTA con su razón. Estaban escritas en el CSS y no se leían desde ninguna parte; ahora la ficha del componente las enseña, que es donde se decide si una sigue siendo cierta. Las 163 fichas cambian porque la plantilla cambia (sección nueva + la línea de reparto); el README de la auditoría gana la fila de la clase y la nota de que `system` y `exception` quedan fuera del denominador. Guards: `--names` 4.105 claves, DESVIADAS 0 · component-audit PASS · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
String(r.exception).padStart(7) +
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
String(r.system).padStart(8) +
feat(theming): la clase structural del censo - el 0% por NATURALEZA deja de contar como deuda Pieza del CIERRE del eje (SS13: el censo no distingue "0% por deuda" de "0% POR NATURALEZA" y el gate de F3 era inalcanzable por construccion). Nueva clase structural en theming-census.ts, la forma de LAYER_VOCABULARY: la lista Y la razon por componente EN el artefacto, por COMPONENTE entero, con el SS5 firmado de cada ficha como fuente. No es un cajon para "este es dificil": una entrada exige veredicto escrito, y un componente que gane superficie de tema real sale de la lista. Los cinco medidos 2026-08-22: aspect-ratio (faceta de box, knob prestado) - text-blur (el 1px de la tecnica sr-only) - cascade y motion (el opacity del gate antiparpadeo, mecanica del canal cuyo valor vive en EidosConfig.motion) - date-picker (la correccion max-content del pie, un unico valor correcto). Aritmetica cuadrada: structural 9 knobs (global -1, literal -8), knobs 4998 intacto (salen del DENOMINADOR, como system), reach 68% -> 69%, no-contract 23 -> 18, <20% 12 -> 7. Diff de tablas: exactamente 5 filas cambian (0% -> strct), las otras 157 byte a byte. Los knobs estructurales se siguen LISTANDO en SS2-bis de su ficha con su razon; la seccion de propuestas los excluye (proponer un token contradiria el SS5). component:audit identico (162 PASS; el NEEDS-WORK de motion es R-1.1, raiz sin declarar, ortogonal al alcance). Suelo del censo verde con margen. Candidatos NO incluidos, reportados con dato: field-langs es deuda REAL (31 globales crudos) - range-calendar/month-grid/year-grid son la pregunta abierta de las capas compartidas - display/heading/text ya son all-system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
String(r.structural).padStart(7) +
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente El autor, al ver que el radio del trigger del nav vivía en un privado clavado a --radius-default: «¿los componentes son themables? si no lo son, es un error como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar moviendo el sistema entero. Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205 knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %. El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens · talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error · panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada token nuevo, desde el píxel hacia arriba, registro de animationstart/end), guards por fichero, un componente = un commit con sus artefactos, y revisión adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho. El instrumento se probó por mutación antes de creerle, y falló la primera vez: no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos mutaciones. Las cifras del plan son las del instrumento final, y el handoff (CONTINUE-theming.md) obliga a reproducirlas antes de seguir. Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del script limpio · tsc del script sin errores · el plan cita ficheros que existen (los once, comprobados). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
String(r.contractKeys).padStart(10) +
(r.hasSize ? ' y' : ' -')
);
}
}
if (process.argv[1] && /theming-census\.ts$/.test(process.argv[1].replace(/\\/g, '/'))) main();

Powered by TurnKey Linux.