@ -35,10 +35,14 @@
* 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 .
* Reach = public / ( public + private + global + literal ) . ` system ` and
* ` exception ` 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 ) .
* 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 .
*
* REPORT . ` --report ` writes the audit under ` docs/audit/theming/ ` : a root
* ` README.md ` with the whole - catalogue view , and one ` {c}.md ` per component
@ -96,7 +100,14 @@ const INERT = new Set([
/** recipe-contract §3's exception valve, as written in the canon. */
const ANNOTATED = new RegExp ( '[/][*][ ]*literal:' ) ;
export type KnobClass = 'public' | 'private' | 'system' | 'global' | 'literal' | 'exception' ;
export type KnobClass =
| 'public'
| 'private'
| 'system'
| 'global'
| 'literal'
| 'exception'
| 'structural' ;
export interface Knob {
file : string ;
@ -128,7 +139,9 @@ export interface CensusRow {
literal : number ;
/** A literal carrying its `/* literal: … * /` annotation — recipe-contract §3. */
exception : number ;
/** public / (public + private + global + literal) — `system`/`exception` out. */
/** Knobs of a component whose 0 % is its nature — `STRUCTURAL_COMPONENTS`. */
structural : number ;
/** public / (public + private + global + literal) — the other three out. */
reach : number ;
contractKeys : number ;
hasSize : boolean ;
@ -192,6 +205,56 @@ const LAYER_VOCABULARY: { layer: string; consumers: Set<string>; needle: RegExp
}
] ;
/ * *
* 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`'
]
] ) ;
function classify (
value : string ,
pubNeedle : string ,
@ -278,6 +341,7 @@ export function scanComponent(dir: string): Scan | null {
global : 0 ,
literal : 0 ,
exception : 0 ,
structural : 0 ,
reach : 0 ,
contractKeys : contractKeysFor ( dir ) ,
hasSize : false
@ -343,7 +407,11 @@ export function scanComponent(dir: string): Scan | null {
continue ;
}
if ( ! KNOB_PROPS . test ( prop ) || INERT . has ( val ) ) continue ;
let klass = classify ( val , pubNeedle , privNeedle , dir ) ;
// 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 ) ;
// 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
@ -409,7 +477,7 @@ export function scanComponent(dir: string): Scan | null {
}
const themeable = row . public + row . private + row . global + row . literal ;
row . knobs = themeable + row . system ;
row . knobs = themeable + row . system + row . structural ;
row . reach = themeable === 0 ? 1 : row.public / themeable ;
const declared = new Set ( privates . map ( ( p ) = > p . name ) ) ;
const privatesFromContract = [ . . . usedPrivates ]
@ -716,7 +784,11 @@ const VERDICT_SEED =
const pct = ( n : number , d : number ) = > ( d === 0 ? '—' : ` ${ Math . round ( ( 100 * n ) / d ) } % ` ) ;
const cell = ( s : string ) = > '`' + s . replace ( /\|/g , '\\|' ) + '`' ;
const reachOf = ( r : CensusRow ) = > pct ( r . public , r . public + r . private + r . global + r . literal ) ;
/** `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 ) ;
const list = ( xs : string [ ] ) = > xs . map ( ( k ) = > '`' + k + '`' ) . join ( ', ' ) ;
/ * *
@ -758,8 +830,15 @@ function knobTable(rows: Knob[], self: string): string {
function proposalSection ( scan : Scan ) : string {
const c = scan . row . component ;
const targets = scan . knobs . filter ( ( k ) = > k . klass !== 'public' && k . klass !== 'system' ) ;
if ( targets . length === 0 ) return '_Nada que proponer: no hay knobs fuera de alcance._\n' ;
// `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' ;
const proposals = targets . flatMap ( ( k ) = > propose ( k , scan ) ) ;
const minted = new Map < string , { value : Set < string > ; scope : string ; uses : number } > ( ) ;
@ -997,8 +1076,12 @@ function sheet(scan: Scan, today: string, verdict: string): string {
> Vista de conjunto : [ README ] ( . / README . md ) · m é todo y protocolo :
> [ \ ` PLAN-theming.md \` ](../../process/PLAN-theming.md) §1, §2, §7.
- * * Medido * * : $ { today } · * * Alcance * * : * * $ { reachOf ( row ) } * * — $ { 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 } _ ( los dos ú ltimos , fuera del ratio ) _
- * * 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 ) _
- * * Contrato hoy * * ( \ ` lib/recipes/base.ts \` ): ${ contractLine }
- * * Eje \ ` size \` **: ${ row . hasSize ? 'sí' : 'no' } · **ficheros**: ${ list ( scan . files ) }
@ -1024,7 +1107,20 @@ ${knobTable(by('exception'), c)}## 2. Sistema transversal (${row.system}) — in
Un tema los alcanza * * a nivel de sistema * * , por dise ñ o ( recipe - contract § 2 ) .
$ { knobTable ( by ( 'system' ) , c ) }
$ { 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 ) } `
: ''
}
# # 3 . Privados de la receta — ¿ de d ó nde sale su valor ?
$ { privateTable ( scan ) } $ {
@ -1054,7 +1150,12 @@ function rootReadme(scans: Scan[], empties: EmptyScan[], today: string, verdict:
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 ) ;
const noContract = rows . filter ( ( r ) = > r . contractKeys === 0 ) . map ( ( r ) = > r . component ) ;
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 ) ;
const worst = [ . . . rows ]
. map ( ( r ) = > ( { c : r.component , out : r.private + r . global + r . literal , r } ) )
. sort ( ( a , b ) = > b . out - a . out )
@ -1073,8 +1174,8 @@ function rootReadme(scans: Scan[], empties: EmptyScan[], today: string, verdict:
- * * 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
- * * Reparto * * : p ú blico $ { sum ( 'public' ) } · privado $ { sum ( 'private' ) } · global $ { sum ( 'global' ) } · literal $ { sum ( 'literal' ) } · sistema transversal $ { sum ( 'system' ) } · excepci ó n firmada $ { sum ( 'exception' ) } _ ( los do s ú 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 }
- * * 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 tre s ú 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 }
# # C ó mo se lee
@ -1086,10 +1187,12 @@ function rootReadme(scans: Scan[], empties: EmptyScan[], today: string, verdict:
| \ ` 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 |
| \ ` 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 |
| \ ` 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 % \` |
* * Alcance * * = \ ` public / (public + private + global + literal) \` . \` system \` y \` exception \`
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 .
* * 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 .
* * 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
@ -1099,6 +1202,18 @@ cuenta como \`global\` y la ficha lo marca «⤴ prestado»; los valores dentro
privado que no deriva , velo en el nodo equivocado , doble animaci ó n — van en la
checklist § 4.4 de cada ficha .
# # 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' ) }
# # Qu é NO propone una ficha
La propuesta deriva nombres de la doctrina ; * * no la contradice * * . Por eso una
@ -1148,12 +1263,12 @@ ${noContract.map((c) => `[\`${c}\`](./${c}.md)`).join(' · ')}
La columna « contrato » cuenta las claves * * p ú blicas * * del bloque del componente .
| componente | alcance | knobs | p ú blico | privado | global | literal | sistema | contrato | size |
| -- - | -- - : | -- - : | -- - : | -- - : | -- - : | -- - : | -- - : | -- - : | :- : |
| componente | alcance | knobs | p ú blico | privado | global | literal | sistema | estructural | contrato | size |
| -- - | -- - : | -- - : | -- - : | -- - : | -- - : | -- - : | -- - : | -- - : | --- : | :- : |
$ { sorted
. map (
( r ) = >
` | [ ${ r . component } ](./ ${ r . component } .md) | ${ reachOf ( r ) } | ${ r . knobs } | ${ r . public } | ${ r . private } | ${ r . global } | ${ r . literal } | ${ r . system } | ${ r . contractKeys} | ${ r . hasSize ? 'y' : '– ' } | `
` | [ ${ 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' : '– ' } | `
)
. join ( '\n' ) }
@ -1610,10 +1725,10 @@ function main() {
const themeable = sum ( 'public' ) + sum ( 'private' ) + sum ( 'global' ) + sum ( 'literal' ) ;
console . log ( ` theming-census — ${ rows . length } component recipe(s) ` ) ;
console . log (
` 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' ) } `
` 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' ) } `
) ;
console . log (
` no contract entry: ${ rows . filter ( ( r ) = > r . contractKeys === 0 ) . 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 } `
` 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 } `
) ;
console . log ( '' ) ;
const sorted = [ . . . rows ] . sort ( ( a , b ) = > a . reach - b . reach || b . knobs - a . knobs ) ;
@ -1627,6 +1742,7 @@ function main() {
'literal' . padStart ( 9 ) +
'excep' . padStart ( 7 ) +
'system' . padStart ( 8 ) +
'strct' . padStart ( 7 ) +
'contract' . padStart ( 10 ) +
' size'
) ;
@ -1641,6 +1757,7 @@ function main() {
String ( r . literal ) . padStart ( 9 ) +
String ( r . exception ) . padStart ( 7 ) +
String ( r . system ) . padStart ( 8 ) +
String ( r . structural ) . padStart ( 7 ) +
String ( r . contractKeys ) . padStart ( 10 ) +
( r . hasSize ? ' y' : ' -' )
) ;