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/__names-codemod.ts

180 lines
7.1 KiB

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
/**
* Codemod value-preserving del vocabulario de tokens (D-TH.6, acta 2026-08-20).
*
* node --import tsx/esm scripts/__names-codemod.ts --dry # sólo mide
* node --import tsx/esm scripts/__names-codemod.ts --write # escribe
*
* Renombra la CLAVE en `recipes/base.ts` y TODAS sus referencias
* `var(--{c}-{clave})` en el árbol indexado por git. No toca valores: un
* rename no cambia lo que se ve ni cuánto alcanza el tema.
*
* Molde: la normalización px/py del 2026-07-06 (recipe-contract §1) y su
* triple muro. Aquí los muros son: colisión de destino · clave duplicada en
* `base.ts` (añadir un bloque que ya existe lo descarta EN SILENCIO) · el
* diff de `generated/` como renombres 1:1.
*
* Lo que NO reescribe, y por qué:
* docs/process/** — actas, planes y handoffs: son HISTORIA
* docs/theming/changelog.md — crónica fechada
* Reescribir un registro histórico lo convierte en una mentira retroactiva.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { resolve } from 'node:path';
import { names, type NameFinding } from './theming-census';
const BASE = resolve('src/uix/eidos/lib/recipes/base.ts');
const HISTORY = /^(docs\/process\/|docs\/theming\/changelog\.md$)/;
const read = (p: string) => readFileSync(p, 'utf8').replace(/\r\n/g, '\n');
/** Claves de primer nivel duplicadas dentro de un mismo bloque de receta. */
function duplicateKeys(src: string): string[] {
const blocks = [...src.matchAll(/^\t'?([a-z0-9-]+)'?: \{/gm)].map((m) => ({
name: m[1],
at: m.index!
}));
const dupes: string[] = [];
for (let i = 0; i < blocks.length; i++) {
const end = i + 1 < blocks.length ? blocks[i + 1].at : src.length;
const seen = new Set<string>();
for (const k of src.slice(blocks[i].at, end).matchAll(/^\t\t'?(_?[a-z0-9-]+)'?\s*:/gm)) {
if (seen.has(k[1])) dupes.push(blocks[i].name + '.' + k[1]);
seen.add(k[1]);
}
}
return dupes;
}
function blockRange(src: string, component: string): [number, number] | null {
const blocks = [...src.matchAll(/^\t'?([a-z0-9-]+)'?: \{/gm)].map((m) => ({
name: m[1],
at: m.index!
}));
const i = blocks.findIndex((b) => b.name === component);
if (i < 0) return null;
return [blocks[i].at, i + 1 < blocks.length ? blocks[i + 1].at : src.length];
}
async function run() {
const write = process.argv.includes('--write');
const findings = await names();
const renames = findings.filter(
(f) => (f.klass === 'ink' || f.klass === 'modifier' || f.klass === 'both') && f.to
);
// ── Muro 1: colisiones ────────────────────────────────────────────────────
const base0 = read(BASE);
const collisions: string[] = [];
const byComponent = new Map<string, NameFinding[]>();
for (const f of renames) byComponent.set(f.component, [...(byComponent.get(f.component) ?? []), f]);
for (const [component, fs] of byComponent) {
const range = blockRange(base0, component);
if (!range) {
collisions.push(`${component}: SIN BLOQUE`);
continue;
}
const existing = new Set(
[...base0.slice(range[0], range[1]).matchAll(/^\t\t'?(_?[a-z0-9-]+)'?\s*:/gm)].map((m) => m[1])
);
const targets = new Set<string>();
for (const f of fs) {
if (existing.has(f.to!) && f.to !== f.key) collisions.push(`${component}.${f.to} YA EXISTE`);
if (targets.has(f.to!)) collisions.push(`${component}.${f.to} destino DUPLICADO`);
targets.add(f.to!);
}
}
// ── Muro 2: claves duplicadas ANTES ───────────────────────────────────────
const dupesBefore = duplicateKeys(base0);
// El PLAN se escribe antes de tocar nada: es lo único que sobrevive al
// renombrado, y sin él el verificador del diff no tiene contra qué medir
// (el clasificador, releído después, ya no ve desviación alguna).
const planPath = resolve('scripts/__names-plan.json');
if (write)
writeFileSync(
planPath,
JSON.stringify(
renames.map((f) => ({ component: f.component, from: f.key, to: f.to })),
null,
'\t'
) + '\n',
'utf8'
);
console.log(`renombres: ${renames.length} en ${byComponent.size} componentes`);
console.log(`muro 1 · colisiones: ${collisions.length ? collisions.join(' | ') : 'ninguna'}`);
console.log(`muro 2 · duplicadas ANTES: ${dupesBefore.length ? dupesBefore.join(' | ') : 'ninguna'}`);
if (collisions.length || dupesBefore.length) {
console.error('\nABORTA: un muro está en rojo.');
process.exit(1);
}
// ── Pase A: la CLAVE en base.ts, acotado a su bloque ──────────────────────
let base = base0;
let keyHits = 0;
for (const [component, fs] of byComponent) {
const range = blockRange(base, component);
if (!range) continue;
let block = base.slice(range[0], range[1]);
// más largas primero: una clave puede ser prefijo de otra
for (const f of [...fs].sort((a, b) => b.key.length - a.key.length)) {
const re = new RegExp("^(\\t\\t)'?" + f.key + "'?(\\s*:)", 'm');
if (!re.test(block)) continue;
const quoted = /-/.test(f.to!) ? `'${f.to}'` : f.to!;
block = block.replace(re, `$1${quoted}$2`);
keyHits++;
}
base = base.slice(0, range[0]) + block + base.slice(range[1]);
}
console.log(`pase A · claves reescritas en base.ts: ${keyHits} / ${renames.length}`);
// ── Pase B: las REFERENCIAS `--{c}-{clave}` en el árbol indexado ──────────
const files = execSync('git ls-files "*.ts" "*.css" "*.svelte" "*.md"', {
encoding: 'utf8',
maxBuffer: 1 << 28
})
.split('\n')
.filter(Boolean)
.filter((f) => !HISTORY.test(f));
// Más largas primero y con frontera: `--waveform-color` no puede comerse
// `--waveform-color-played`.
const refs = [...renames].sort((a, b) => b.key.length - a.key.length);
let refHits = 0;
const touched: string[] = [];
for (const file of files) {
const before = file === 'src/uix/eidos/lib/recipes/base.ts' ? base : read(file);
let after = before;
for (const f of refs) {
const from = `--${f.component}-${f.key}`;
const re = new RegExp(from.replace(/-/g, '\\-') + '(?![a-zA-Z0-9-])', 'g');
const n = (after.match(re) ?? []).length;
if (!n) continue;
after = after.replace(re, `--${f.component}-${f.to}`);
refHits += n;
}
if (after === before) continue;
touched.push(file);
if (write) writeFileSync(file, after, 'utf8');
}
if (write && !touched.includes('src/uix/eidos/lib/recipes/base.ts'))
writeFileSync(BASE, base, 'utf8');
console.log(`pase B · referencias reescritas: ${refHits} en ${touched.length} ficheros`);
// ── Muro 3: claves duplicadas DESPUÉS ─────────────────────────────────────
const dupesAfter = duplicateKeys(base);
console.log(`muro 3 · duplicadas DESPUÉS: ${dupesAfter.length ? dupesAfter.join(' | ') : 'ninguna'}`);
if (dupesAfter.length) {
console.error('\nABORTA: el renombrado creó una clave duplicada.');
process.exit(1);
}
if (!write) console.log('\n(--dry: no se ha escrito nada)');
}
void run();

Powered by TurnKey Linux.