test(theming): el suelo del alcance, con sus dos muta-pruebas — cierra F0.d

`src/uix/eidos/theming-reach-floor.test.ts`. El plan lo encargaba en F0 («un
test que fije el suelo: el número SUBE o el test falla») y nunca se escribió,
así que hasta hoy **nada impedía que el alcance BAJARA entre sesiones**: una
receta podía cambiar un token público por un primitivo y sólo se vería leyendo
el censo a mano.

Cuatro trinquetes, medidos el 2026-08-22 tras el endurecimiento del censo:
alcance global ≥ **56 %** · literales ≤ **564** · knobs sobre global crudo ≤
**1.156** · componentes al 100 % ≥ **14**. Cuando el eje avanza, se suben en el
mismo commit que se lo gana.

**LAS DOS MUTA-PRUEBAS** (memoria `a-guard-that-inspects-nothing-passes`: un
guard sólo vale cuando lo has VISTO rojo). No se dan por supuestas — se
ejecutaron:

1. Inyectar UN literal (`border-radius: 7px` en `mark`) → rojo en `maxLiteral`
   y en la sonda del clasificador. Revertido, verde, árbol idéntico.
2. Cambiar CINCO públicos de `tree-view` por primitivos crudos (92 % → 73 % en
   ese componente) → rojo en `maxGlobal`. Revertido, vuelve a 92 %.

Y de ahí sale una observación que queda escrita en el propio test: **los techos
son el detector FINO y el porcentaje el grueso.** Cinco knobs de ~4.700 no
mueven un porcentaje redondeado, pero sí un techo — una regresión demasiado
pequeña para esconderse de `reachPct` sigue disparando `maxGlobal`.

Lo que el test NO asegura, a propósito: el alcance POR COMPONENTE, que ya
gobiernan el censo `--only` y las excepciones R-5.x — un número bajo puede ser
doctrina bien aplicada (`menu-dial` paga por consumir bien una capa compartida,
`link` por heredar la tipografía ambiente). Y `system`, que es temable a nivel
de sistema por diseño y queda fuera del ratio.

Gates: los 5 tests en verde · suite eidos 439/440 (el único rojo sigue siendo
el conocido `skin-media-player`) · prettier sobre el fichero nuevo · docs:check
0 · check limpio. `PLAN §5 F0.d` y la entrada de §13 quedan cerradas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent a9acf8d0c4
commit 3a47c1a53d

@ -695,7 +695,13 @@ Lo que `textarea` añadió (2026-08-21):
adjudicados por eso. Misma clase que `date-range-picker` con
`kind='month'|'year'` (norma N-6).
- **⚠ El test del SUELO del censo que F0 pedía NO EXISTE.** El plan (§5, F0.d)
- ~~**El test del SUELO del censo que F0 pedía no existe.**~~ **ESCRITO
2026-08-22** — `src/uix/eidos/theming-reach-floor.test.ts`: alcance global
≥ 56 %, literales ≤ 564, globales ≤ 1.156 y ≥ 14 componentes al 100 %, con
las dos muta-pruebas que el plan exige (un literal inyectado y cinco públicos
cambiados por primitivos, cada uno con su rojo). Medido de paso: **los techos
son el detector FINO y el porcentaje el grueso** — cinco knobs de ~4.700 no
mueven un porcentaje redondeado, pero sí un techo. La entrada original decía: El plan (§5, F0.d)
encarga «un test que fije el suelo: alcance global ≥ el de hoy y literales ≤
614 — el número SUBE o el test falla», con su muta-prueba. No hay
`theming-census.test.ts` ni equivalente bajo `src/uix/eidos/`, así que hoy

@ -226,8 +226,8 @@ falla mucho».
reclasificar), salida `--json` estable; (b) **R-5.1/5.2/5.3 en
`component-audit`** con severidad `warn` y la válvula `R-5.x exception:`;
(c) `docs/theming/reference.md` §12 (tabla de tooling) gana la fila del censo;
(d) un test `scripts/theming-census.test.ts` o bajo `src/uix/eidos/` que
**fije el suelo**: alcance global ≥ el de hoy (33 %) y literales ≤ 614 — el
(d) **HECHO 2026-08-22** — `src/uix/eidos/theming-reach-floor.test.ts`, que
**fija el suelo**: alcance global ≥ el de hoy (33 %) y literales ≤ 614 — el
número SUBE o el test falla (muta-prueba: meter un literal en una receta y ver
el rojo antes de dar el test por bueno — memoria `a-guard-that-inspects-nothing-passes`.
Ya pasó una vez HOY: la primera versión del censo no contaba una regla de una

@ -0,0 +1,106 @@
import { describe, expect, it } from 'vitest';
import { census, scanComponent } from '../../../scripts/theming-census';
/**
* The FLOOR of the theme-reach axis (PLAN-theming.md §5, F0.d).
*
* The plan asked for this in F0 and it was never written, so until 2026-08-22
* nothing stopped the reach from going DOWN between sessions: a recipe could
* quietly swap a public token for a raw primitive and only a human reading the
* census by hand would notice. These numbers are a RATCHET — when the axis
* advances, they are raised in the same commit that earns it.
*
* The complement matters as much as the percentage: `literal` and `global`
* knobs are the debt, and they may only shrink.
*
* Deliberately NOT asserted here:
* - per-component reach, which the census `--only` and the R-5.x exceptions
* already govern (a low number can be correct doctrine — `menu-dial` pays
* for consuming a shared layer properly, `link` for inheriting ambient
* typography);
* - `system`, which is themeable at system level by design and sits outside
* the ratio (recipe-contract §2).
*/
/**
* What each assertion actually catches, measured with two mutations
* (2026-08-22) rather than assumed:
*
* - injecting ONE literal into a recipe (`border-radius: 7px` in `mark`)
* turns `maxLiteral` and the probe red;
* - swapping FIVE publics for raw primitives in `tree-view` (92 % -> 73 % for
* that component) turns `maxGlobal` red, while `reachPct` survives — five
* knobs out of ~4.700 cannot move a rounded global percentage.
*
* So the ceilings are the FINE detector and the percentage is the coarse one.
* A regression small enough to hide from `reachPct` still trips a ceiling.
*/
// Measured 2026-08-22, after the census learned that a DERIVED private reaches.
const FLOOR = {
reachPct: 56,
maxLiteral: 564,
maxGlobal: 1156,
minAtHundred: 14
};
describe('theming reach — the floor only moves up', () => {
const rows = census();
const totals = rows.reduce(
(acc, r) => {
acc.public += r.public;
acc.private += r.private;
acc.global += r.global;
acc.literal += r.literal;
return acc;
},
{ public: 0, private: 0, global: 0, literal: 0 }
);
const themeable = totals.public + totals.private + totals.global + totals.literal;
const reachPct = Math.round((totals.public / themeable) * 100);
it(`global reach is at least ${FLOOR.reachPct}%`, () => {
expect(
reachPct,
`reach fell to ${reachPct}% (floor ${FLOOR.reachPct}%). A recipe traded a public token ` +
'for a primitive or a literal — find it with `theming-census.ts --only <c>`.'
).toBeGreaterThanOrEqual(FLOOR.reachPct);
});
it(`literal knobs never grow past ${FLOOR.maxLiteral}`, () => {
expect(
totals.literal,
`${totals.literal} literals (ceiling ${FLOOR.maxLiteral}). A hard-coded value entered a recipe.`
).toBeLessThanOrEqual(FLOOR.maxLiteral);
});
it(`knobs reading a raw global never grow past ${FLOOR.maxGlobal}`, () => {
expect(totals.global).toBeLessThanOrEqual(FLOOR.maxGlobal);
});
it(`at least ${FLOOR.minAtHundred} components are fully reachable`, () => {
const atHundred = rows.filter((r) => r.reach === 1 && r.knobs > 0).length;
expect(atHundred).toBeGreaterThanOrEqual(FLOOR.minAtHundred);
});
/**
* The mutation test the plan demands (memory
* `a-guard-that-inspects-nothing-passes`): a guard is only trustworthy once
* you have SEEN it go red. This one proves the classifier still reacts —
* without it, a broken scanner would report a perfect catalogue and every
* assertion above would pass over nothing.
*/
it('the classifier still reacts to a literal (mutation probe)', () => {
const scan = scanComponent('mark');
expect(scan, 'mark should scan').not.toBeNull();
const before = scan!.row.literal;
// `mark` was tokenized on 2026-08-22 with zero literals; if that ever
// changes this probe needs a different subject, and the failure says so.
expect(
before,
'mark is no longer literal-free — pick another subject for the mutation probe'
).toBe(0);
expect(scan!.row.public).toBeGreaterThan(0);
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.