Saltar a contenido

Refactorización KISS — diagnóstico y plan

Fecha de la línea base: 1 de noviembre de 2026. Base medida: main de los 14 repos, con los CI en verde.

Documentos de referencia:

1. Qué se ha medido

Métrica Valor en la línea base
Repos inventariados 14
Ficheros PHP propios 429
Líneas de PHP propio (sin vendor/tests/build) 127.690
Shortcodes 38
Tipos de contenido 8
Rutas REST 69
Acciones AJAX 35
Eventos cron 35
Opciones 111
Hooks emitidos / escuchados 169 / 428
Enlaces entre plugins por hook 59
Ficheros de prueba / métodos de prueba 805 / ~5.712
Métodos de 90 líneas o más 191
Clones reales entre plugins (bloques de 8 líneas) 1 fichero (~200 líneas)

Corrección del instrumento (1/11/2026). La primera pasada guardaba 21 rutas REST porque solo reconocía el espacio de nombres escrito como literal. Los plugins que usan register_rest_route( self::NAMESPACE, ... ) quedaban fuera: Members declaraba 29 y el baseline guardaba una. Resueltas las constantes del fichero, el inventario pasa de 973 a 1.021 capacidades. La cifra anterior era optimista, no el código el que estaba mal.

Los números se regeneran con:

cd ~/repos/convoca-docs
python3 bin/convoca-inventario.py        # mide y escribe docs/refactor/inventario.json
python3 bin/convoca-inventario-md.py     # presenta
python3 bin/convoca-inventario.py --check # falla si desaparece una capacidad

2. Diagnóstico honesto

El ecosistema está mejor de lo que sugiere el nombre de las clases.

El escaneo por nombres de clase encuentra 29 clases repetidas (Logger, Utils, CSV_Exporter, Payment_Listener…), pero al abrirlas se ve que casi todas son la misma clase de Core vista desde el stub de PHPStan de otro plugin, no copias reales. Un detector de clones por bloques normalizados sobre los 356 ficheros con contenido encuentra un solo duplicado real: Admin_Status en Members y Shifts.

Y donde había duplicación histórica ya se corrigió: las tres exportaciones CSV de Enroll, Members y Gateway ya llaman a \Convoca\Core\Utils::stream_csv() y a escape_csv_field(). Lo que cada plugin conserva es su consulta (negocio), no la mecánica (infraestructura). Eso es exactamente el reparto que se pide.

Por tanto, la deuda real no es "código repetido". Es:

  1. Dos ficheros gemelos que se van a desincronizar (Admin_Status).
  2. Cuatro copias a mano de los stubs de PHPStan de Core (coste de mantenimiento y falsos resultados en el análisis estático).
  3. 191 métodos de 90 líneas o más, muchos de ellos declarativos (registro de ajustes, de rutas, de plantillas) que se pueden partir por secciones sin cambiar comportamiento.
  4. Acoplamiento oculto por opciones: 9 opciones las lee más de un plugin, sin filtro ni documentación. Funciona, pero es un contrato no escrito.
  5. Una dependencia no declarada: assistant usa Convoca\Core sin Requires Plugins: convoca-core.
  6. Tres copias de chillerlan/php-qrcode (548 KB cada una) que sí viajan en los paquetes de producción.

No se ha encontrado código muerto: los 14 candidatos que salieron (widgets de Shifts, canales de Publisher, Admin_Actividades_List) se registran por cadena o por nombre de clase dinámico. No se borra nada de eso.

3. Plan por fases y riesgo

Cada punto es un cambio aislado, en su rama, con su verificación y su rollback. Nada se fusiona sin CI verde y sin --check del inventario.

Estado de ejecución (1/11/2026)

# Cambio Estado
A1 Requires Plugins: convoca-core en Assistant hecho — 0.2.26, 288/288 pruebas
A2 Admin_Status: renderizador único en Core hecho y desplegado — core 2.3.53, members 2.11.8, shifts 2.5.39 en demo y Lugg
A4 Puerta de inventario en el empaquetado hecho — probada en negativo
A3 Generador único de stubs PHPStan pendiente
B4 Documentar las opciones compartidas como contrato hecho — opciones-compartidas.md
B2 register_routes() de Members partido por recurso descartado por medición — ver abajo
B1, B3, B5 register_settings() (374 l.), default_templates() (317 l.), filtros pendiente
C1–C2 qrcode a Core, esqueleto de pago pendiente (riesgo medio)
D1–D3 Rediseños no se ejecutan sin decisión

Efecto medido de A2: el único clon real entre plugins que quedaba (Admin_Status, 69 bloques de 8 líneas idénticos) ya no existe; el detector de clones solo encuentra las copias de los stubs de PHPStan, que no se despliegan. Las dos clases pasan de 200 y 201 líneas a 127 y 123 porque el HTML y las comprobaciones genéricas viven ahora una sola vez en Core. La línea total no baja: sube, porque la versión compartida está documentada. El objetivo es una implementación en lugar de dos, no menos líneas.

Despliegue verificado el 1/11/2026: paquetes construidos desde HEAD, copia previa por plugin, md5 407/407, 168/168 y 68/68 en demo y en Lugg, y ejecución real de las dos pantallas con wp eval-file en ambos sitios (4 comprobaciones por plugin, ids y severidades correctos). Sin errores PHP nuevos en los logs después del despliegue.

B2, descartado con datos (1/11/2026). El método de 337 líneas se partió en siete, con las 27 rutas capturadas idénticas antes y después. El gate de calidad del repo lo rechazó: PHPMD subió de 234 a 235. La causa es que el problema no era el método sino la clase (Rest_API: 1.766 líneas, 30 métodos, GodClass); añadir métodos empeora las métricas de clase aunque cada método sea más corto. Se probó con diez bloques y el total empeoró más. Revertido en 7e25e57. Conclusión para el plan: partir métodos dentro de una clase dios no es una mejora medible; lo que corresponde es partir la clase (P3, rediseño con migración de pruebas), no trocear el método.

Nota de instrumento: el phpmd.phar del taller no es el mismo que el fijado por el CI (sobre el fichero intacto da 237 frente a 234 del CI), así que los totales locales solo sirven como orientación. La cifra que manda es la del gate en el CI.

P0 — Sin riesgo, se hace ya

# Cambio Repos Verificación
A1 Declarar Requires Plugins: convoca-core en Assistant assistant cabecera + CI + inventario
A2 Admin_Status: un renderizador de diagnóstico en Core, datos en cada plugin core, shifts, members PHPUnit de Core + pantalla idéntica en demo/Lugg
A3 Generador único de stubs PHPStan de Core; las copias pasan a ser salida core + 4 stubs phpstan analyse de los 4 repos
A4 Puerta de inventario en el empaquetado release --check en seco + prueba negativa

P1 — Riesgo bajo, con prueba que lo cubre

# Cambio Repos Por qué es seguro
B1 register_settings() de Gateway (374 l.) partido por secciones gateway extracción pura; los ajustes y sus nombres no cambian
B2 register_routes() de Members (337 l.) partido por recurso members extracción pura; las 29 rutas de Members se comparan por REST antes/después
B3 default_templates() de Members (317 l.) a un array de datos members mismos literales, mismo orden
B4 Documentar las 9 opciones compartidas como API de facto docs solo documentación
B5 Sustituir los get_option() cruzados nuevos por filtros (sin tocar los existentes) todos aditivo

P2 — Riesgo medio: requiere plan de migración y ventana

# Cambio Repos Riesgo
C1 chillerlan/php-qrcode a Core core, enroll, members, courses descartado con motivo: obliga a subir el suelo de todo el ecosistema para ahorrar 1,1 MB
C2 Payment_Listener de Enroll y Members a un esqueleto común core, enroll, members semántica de pago distinta; se unifica solo el candado de deduplicación

P3 — Rediseño, no se ejecuta sin decisión de JC

# Cambio Por qué no ahora
D1 enroll/media/ y enroll/social/ a Publisher mueve 8 tablas y 5 rutas REST; necesita migración de datos y redirección de rutas
D2 Hour_Ledger de Core a Shifts/Members lo usan dos plugins y es fuente de verdad de horas; moverlo rompe ambos
D3 Adelgazar publisher/includes/class-admin.php (2.818 l.) 60 métodos; partirlo sin pruebas de interfaz es cosmética con riesgo
D4 Descomponer Rest_API de Members en marcha: avisos, privacidad y certificados fuera (1.752→1.587 l., 24→21 métodos, 27 rutas idénticas); siguen perfil, horas y búsqueda

4. Criterio de compatibilidad (regla dura)

Un cambio no se ejecuta si cumple alguna de estas:

  • Renombra o elimina un shortcode, CPT, taxonomía, ruta REST, evento cron, opción o hook de la lista congelada.
  • Cambia la firma de un hook que ya está en uso por otro plugin.
  • Requiere migración de datos sin script reversible y probado sobre una copia.

En esos casos: se documenta el problema en este repositorio, se escribe el plan de migración y se para. No se improvisa.

5. Verificación de que no se pierde nada

  1. python3 bin/convoca-inventario.py --check antes de cada fusión. Falla si desaparece cualquier capacidad del baseline.
  2. Las baterías de los tres sitios (demo, Lugg, biodevas) con 0 FAIL.
  3. Pantallas y flujos del inventario ejercitados tras cada despliegue.
  4. Comparación de rutas REST registradas (wp eval) antes/después en la demo.

Contraste con el sitio real (1/11/2026). En la demo, rest_get_server() devuelve 101 rutas con «convoca» en el nombre; el inventario estático guarda 68 y ninguna de las 68 falta en el sitio. La diferencia son rutas que WordPress genera solo, sobre todo las de los tipos de contenido con show_in_rest (/wp/v2/convoca_curso, /wp/v2/convoca_faq) y las raíces de cada espacio de nombres. Es decir: el baseline es un subconjunto conservador de la realidad. Consecuencia honesta: garantiza que no desaparece lo que vigila, pero no demuestra que exista todo lo que hay. Para el refactor sirve; para afirmar «están todas» no.

El inventario es la prueba, no la promesa.