/** * 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(); 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(); 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(); 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();