`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
parent
a9acf8d0c4
commit
3a47c1a53d
@ -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…
Reference in new issue