From 3a47c1a53d9305c5ba8348098d86a8599bd7886e Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 22 Aug 2026 03:04:47 +0200 Subject: [PATCH] =?UTF-8?q?test(theming):=20el=20suelo=20del=20alcance,=20?= =?UTF-8?q?con=20sus=20dos=20muta-pruebas=20=E2=80=94=20cierra=20F0.d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- docs/next-features.md | 8 +- docs/process/PLAN-theming.md | 4 +- src/uix/eidos/theming-reach-floor.test.ts | 106 ++++++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/uix/eidos/theming-reach-floor.test.ts diff --git a/docs/next-features.md b/docs/next-features.md index d5847e9bb..1ec381943 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -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 diff --git a/docs/process/PLAN-theming.md b/docs/process/PLAN-theming.md index cb4b46193..8785b49be 100644 --- a/docs/process/PLAN-theming.md +++ b/docs/process/PLAN-theming.md @@ -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 diff --git a/src/uix/eidos/theming-reach-floor.test.ts b/src/uix/eidos/theming-reach-floor.test.ts new file mode 100644 index 000000000..f80c1594e --- /dev/null +++ b/src/uix/eidos/theming-reach-floor.test.ts @@ -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 `.' + ).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); + }); +});