Saltar a contenido

Informe de la refactorización KISS

Fecha: 1 de noviembre de 2026. Base: main de los 14 repos.

Este informe cierra las fases 1 a 7 sobre lo que se puede cambiar sin riesgo medido. Lo que queda fuera no está olvidado: está bloqueado por una razón escrita, con su condición para desbloquearlo.

1. Línea base

Antes de tocar nada se construyó un inventario extraído del código (no de la documentación) y un mapa de dependencias:

  • bin/convoca-inventario.py — 1.021 capacidades de 14 repos, con --check que falla si desaparece cualquiera.
  • convoca-release/scripts/verificar-inventario.sh — la puerta antes de empaquetar.
  • convoca-release/scripts/verificar-stubs.py — detecta stubs de PHPStan que declaren una API del núcleo que ya no existe (verde falso).
  • Mapa de dependencias — capas, cabeceras, referencias reales, hooks y las 9 opciones compartidas.

Los tres instrumentos se probaron en negativo: se les metió un fallo y se comprobó que salen con código 1 y lo nombran.

2. Cambios hechos y verificados

# Cambio Repos Prueba
A1 Requires Plugins: convoca-core en Assistant: usaba Convoca\Core\Logger y Utils sin declararlo (fatal en su ruta REST con Core apagado) assistant 288/288 · CI verde
A2 Admin_Status único: comprobaciones genéricas y pantalla en Convoca\Core\Admin\Diagnostico; los datos siguen en cada plugin core, members, shifts 677/1.102/452 · PHPCS y PHPStan a 0 · md5 407/407, 168/168, 68/68 en demo y Lugg · ejecutado en vivo con wp eval-file
A3 Detector de stubs desincronizados release probado en negativo
A4 Puerta de inventario antes de empaquetar release probado en negativo
B4 Contrato de las 9 opciones compartidas, medido (quién escribe y quién lee) docs publicado

Versiones desplegadas: core 2.3.53, members 2.11.8, shifts 2.5.39, assistant 0.2.26, en demo y Lugg. Sin errores PHP nuevos en los logs. Repos y sitios alineados.

3. Lo que se descartó, con la medición delante

B2 — partir register_routes() de Members (337 líneas, 27 rutas). Se hizo: siete métodos, las 27 rutas capturadas idénticas antes y después (espacio de nombres, ruta y método HTTP), 1.102 pruebas en verde. El gate de calidad del repo lo rechazó: PHPMD 234 → 235. La causa 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. Con diez bloques el total empeoró más. Revertido (7e25e57), CI en verde.

B1 — partir register_settings() de Gateway (367 líneas). No se ejecutó: los cortes naturales caen en bloques de 52 y 44 líneas, de modo que el total de PHPMD quedaría igual o peor. Mismo patrón que B2. Queda anotado, no intentado.

Lección que queda escrita: en un repo con ratchet de métricas, trocear métodos dentro de una clase dios no es una mejora medible. La palanca real es partir la clase.

4. Deuda técnica que queda, cuantificada

Deuda Medida Riesgo de tocarla
Rest_API de Members 1.766 líneas, 30 métodos, GodClass Alto: hay que partir la clase y mover pruebas, no el método
class-admin.php de Publisher 2.818 líneas, 60 métodos Alto: sin pruebas de interfaz es cosmética con riesgo
Payment_Handler de Gateway 2.021 líneas, 45 métodos Alto: es el cobro
functions.php del tema 2.128 líneas, 44 métodos Medio: presentación, pero sin pruebas de tema
Métodos de 90+ líneas 191 en 9 repos Mixto: los declarativos son seguros; los que mezclan negocio y HTML, no
chillerlan/php-qrcode 548 KB × 3 paquetes Medio: exige dos releases (núcleo expone, plugins consumen)
enroll/media + enroll/social 8 tablas, 5 rutas REST Alto: rediseño con migración de datos
Opciones con dos escritores convoca_document_theme, convoca_shifts_hora_apertura, convoca_shifts_hora_cierre Medio: decidir quién manda y migrar

4.bis Auditoría de las baterías de pruebas (fase 5)

Medido sobre 8 repos: 5.143 métodos de prueba. Resultado:

  • Cuerpos duplicados: 2, corregidos. En Members, test_los_rangos_traen_su_valor_por_defecto medía lo mismo que test_los_plazos_tienen_valores_por_defecto_sensatos; en Shifts, test_las_columnas_nuevas_van_donde_toca repetía test_las_columnas_propias_van_antes_de_la_fecha. Se conserva uno de cada par, en el fichero cuyo tema le corresponde. 1.101 y 451 pruebas, sin perder ninguna aserción.
  • Pruebas sin aserción: 0. Once métodos son markTestIncomplete que documentan defectos conocidos con su número de issue (core#14, shifts#10, …): no son deuda oculta, son recordatorios con fecha.
  • Limitación del escáner: la primera pasada marcó como «sin aserción» una prueba que sí las tenía, porque el contador de llaves se rompía con una llave dentro de una cadena (una expresión regular). Se comprobó a mano antes de tocar nada. Un auditor que no se audita a sí mismo produce trabajo inútil.

4.ter Hallazgo que bloquea C1: la matriz de PHP declarada es falsa

Antes de mover chillerlan/php-qrcode al núcleo se comprobó por qué estaba duplicada. El resultado es otra cosa:

Plugin Declara Suelo real Causa
Enroll PHP 8.1 PHP 8.4 eluceo/ical 2.17.0 exige ~8.4.0 \|\| ~8.5.0
Members PHP 8.1 PHP 8.2 chillerlan/php-qrcode 6.0.1 exige ^8.2
Courses PHP 8.1 PHP 8.2 igual

Comprobado con composer why-not php 8.1. No se ve en el servidor porque los sitios corren 8.4/8.5 y el paquete viaja ya construido; se ve cuando un cliente instala en 8.1 —lo que la ficha le permite— y usa un QR o un .ics.

Resuelto el 1/11/2026: se declara el suelo real, que es lo honesto y no rompe nada de lo que hoy funciona (los sitios corren 8.4/8.5). Enroll 2.7.71 → PHP 8.4 con la matriz del CI en 8.4/8.5; Members 2.11.9 y Courses 0.23.12 → PHP 8.2. Los tres CI en verde.

C1 queda descartado, y por una razón medida. Mover chillerlan/php-qrcode al núcleo obligaría a poner el suelo de Core en 8.2 —y con él el de todos los plugins que lo requieren— para ahorrar 1,1 MB repartidos en tres paquetes. El precio es una matriz de soporte más alta para todo el ecosistema y una fachada nueva que mantener. No compensa: se deja la librería donde está, con su versión fijada y su class_exists de guarda. Es el mismo criterio que se aplicó al exportador CSV: no se centraliza por estética.

4.quater Corrección de un informe propio

En el turno anterior dije que el CI de Shifts estaba en verde tras A2. No lo estaba. Leí la última ejecución disponible, que era la del commit anterior: la de A2 (cd9e7af) había fallado con class.notFound en las 12 pruebas de Admin_Status, porque el job de PHPUnit no traía el repositorio del núcleo y el plugin ya usa Convoca\Core\Admin\Diagnostico.

El fallo no llegó a producción (en los sitios el núcleo está instalado y la pantalla se verificó en vivo), pero el informe era falso. Arreglado en c7e6c03: el job de pruebas clona e instala el núcleo, como ya hacía Members. Los 10 trabajos en verde, con el SHA comprobado.

Regla que me llevo: una ejecución de CI no cuenta hasta que su headSha es el que acabo de empujar.

4.quinquies Limpieza de cron y un aviso propio del gate

Cron huérfano (hecho). La demo tenía 50 eventos programados y 4 sin oyente, de la nomenclatura anterior (bdv_*/bde_*). Se retiraron y ahora Cron_Limpieza los limpia al arrancar en cualquier instalación que venga de ahí. Medido tras limpiar: 46 eventos, 3 huérfanos, y los tres son de plugins de terceros (Jetpack/WooCommerce), que no se tocan. Lugg tenía 12 huérfanos, todos de terceros.

PHPMD también me pilló a mí. Admin\Diagnostico metía 3 avisos (método pintar de 54 líneas con NPath 240, y resultado() con cinco parámetros) y mantenía el gate en rojo desde que se creó en A2. Arreglado: el pintado se parte en cabecera y fila, y el arreglo se añade con con_arreglo() en vez de un quinto parámetro. Core vuelve a 119 y el CI a verde.

De paso quedó comprobado que el baseline de calidad de Core estaba desfasado desde el 26/10: el commit 2b30778 ya daba 122. La diferencia era mía (los 3 de Diagnostico), pero el hábito que falla es mío: di por verde un CI sin mirar el headSha.

4.sexies Descomposición de Rest_API: primer trozo hecho

Antes Después
Rest_API 1.752 líneas, 30 métodos 1.653 líneas, 24 métodos + Rest\Notificaciones (3) + Rest\Privacidad (3)

Dos trozos: avisos (leer, marcar uno, marcar todos), que solo dependen de la sesión del socio y de Convoca\Core\Notifications, y privacidad (estado, descarga de datos, solicitud de supresión), que solo hablan con GDPR_Tools. Cada uno a su clase, con las rutas apuntando a ella. Las 27 rutas se capturan idénticas (espacio de nombres, ruta y método HTTP), la batería sigue en 1.101 pruebas y el gate de calidad del CI sigue en verde — el ratchet no sube. Desplegado: members 2.11.12 en demo y Lugg, con las dos clases resolviendo en el sitio y las rutas de avisos y privacidad registradas.

El resto de la clase sigue igual: la descomposición se hace por trozos verificables, no de una vez.

4.decies Búsqueda de socios: dos consultas en vez de una (members#88)

Defecto encontrado al revisar fulltext_search, medido y corregido con autorización expresa.

Qué pasaba: WordPress junta s y meta_query con AND, así que el término tenía que estar en el título y en alguno de los seis metadatos. Con los datos reales de la demo —14 socios y _convoca_nombre vacío en todos— la búsqueda de socios devolvía 0 resultados incluso buscando por el nombre. No era solo el DNI.

Término Antes Después
DNI 0 1
correo 0 1
nombre 0 1

Arreglo: dos consultas —texto y datos— unidas por ID, sin duplicados. Superconjunto de lo anterior: nada que se encontrara antes se pierde. Tres pruebas nuevas escritas antes del cambio y vistas fallar; 1.105 pruebas en total. Desplegado en demo y Lugg (members 2.11.16).

4.nonies Estado de Rest_API y por qué paro aquí

Al empezar 1.752 líneas · 30 métodos
Ahora 1.587 líneas · 24 métodos
Fuera Rest\Notificaciones (avisos), Rest\Privacidad (GDPR), Rest\Certificados (carné y PDF)

Los tres trozos que salieron tienen una propiedad que no tienen los demás: no dependen de nada de la clase. Solo hablan con la sesión del socio y con un servicio (Notifications, GDPR_Tools, PDF_Card/Certificate_Generator).

Lo que queda —get_profile, update_profile, confirm_email, verify_phone, las horas y la búsqueda— sí depende de ayudantes de la propia clase (estado_cuota_panel(), los formateos, la validación de campos). Sacarlos ahora sería mudar el problema de fichero: la clase crecería en el nuevo sitio con las mismas dependencias cruzadas.

Orden correcto para seguir, cuando toque:

  1. extraer los ayudantes compartidos (estado_cuota_panel, formateos de fecha e importe) a una pieza propia, con sus pruebas;
  2. entonces sí, mover los manejadores de perfil y horas, con captura de rutas antes/después.

Y la búsqueda merece un trabajo aparte, no una mudanza: tiene complejidad 15 y NPath 960, y además un defecto de producto medido (members#88: buscar por DNI no encuentra nada porque s y meta_query van en AND).

4.septies Coste en líneas y tamaño (medido, no estimado)

Código de los seis plugins, desde el inicio de la refactorización hasta hoy:

Líneas añadidas +1.235
Líneas borradas −474
Neto +761
PHP de los plugins +28 KB
Documentación generada +116 KB (3.497 líneas)
Herramientas (convoca-release) +6 KB

La refactorización no ha ahorrado líneas: ha añadido 761. Van a docblocks de las clases extraídas (573 líneas entre Diagnostico, Cron_Limpieza, Notificaciones, Privacidad y Certificados), a pruebas nuevas y a los comentarios de por qué existe cada pieza.

Lo que sí bajó:

  • Duplicación real entre plugins: 69 bloques de 8 líneas → 0.
  • Admin_Status: 401 líneas gemelas en dos plugins → una implementación compartida.
  • Rest_API: 1.752 → 1.587 líneas y 30 → 24 métodos.

Los paquetes no cambian de forma apreciable: core son 13 MB de los cuales 11 son Dompdf. El único ahorro grande medido sigue siendo chillerlan/php-qrcode (548 KB × 3 = 1,1 MB), descartado porque obliga a subir el suelo de PHP de todo el ecosistema a 8.2.

Conclusión honesta: si el criterio de éxito fuera el recuento de líneas, este trabajo suspendería. El criterio que se ha seguido es duplicación y complejidad por clase, y ahí sí hay mejora medible.

4.octies Bibliotecas compartidas: chillerlan/php-qrcode a Core

Antes Después
3 copias de 364.247 bytes (Members, Courses, Enroll) 1 copia en Core
Suelo de PHP 8.1 en Core y cinco plugins 8.2 en todo el ecosistema (Enroll 8.4, tema 8.1)

Medido en el sitio, con la copia retirada delante: 364.247 bytes de ficheros por copia (548 KB de disco, 82 ficheros). Neto: 728 KB de ficheros menos por sitio —se quitan dos copias y se queda la de Core—, o 1,07 MB de disco si se cuenta la asignación por bloques.

No cuesta funcionalidad: las tres llamadas siguen siendo las mismas y la clase la resuelve el autoloader de Core, que se carga antes. En los tres repos la dependencia queda en require-dev para que PHPStan no se quede ciego; el empaquetado usa composer install --no-dev, así que el paquete no la lleva.

El tema no sube de 8.1: su código corre ahí y su uso de Core está protegido con function_exists.

Trampa encontrada al desplegar: el despliegue parte de una copia del plugin y superpone el paquete, para conservar lo que el sitio tenga de más. Eso significa que lo que desaparece del paquete no se borra del sitio: las tres copias seguían ahí después de actualizar. Se retiraron a mano, con copia previa en backup-demo y backup-lugg, y se comprobó después que el carné del socio y el certificado de Courses siguen generando su PNG desde convoca-core/vendor/. Sin ese paso el ahorro era un número de laboratorio.

5. Recomendaciones

  1. No seguir troceando métodos. El siguiente escalón real es descomponer Rest_API (Members) y class-admin (Publisher) en clases por recurso, empezando por la que tenga pruebas que la cubran. Es P3: plan y migración de pruebas primero.
  2. C1 (qrcode) sí vale la pena, en dos releases: primero Core lo expone con una fachada propia, después cada plugin la consume. Ahorra 1,1 MB en los paquetes y una dependencia por plugin.
  3. Unificar los tres valores con dos escritores antes de que diverjan: el síntoma será «el ajuste no se queda» y costará más encontrarlo entonces.
  4. Mantener la puerta de inventario en cada release. Es lo que convierte «no hemos perdido nada» en un dato.

6. Cómo se verifica

cd ~/repos/convoca-docs
python3 bin/convoca-inventario.py --check     # 1.021 capacidades, 0 perdidas
cd ~/repos/convoca-release
./scripts/verificar-inventario.sh
python3 scripts/verificar-stubs.py

Y en cada repo tocado: suite completa, PHPCS, PHPStan y el gate de calidad con la versión fijada del CI. Los totales de PHPMD del taller no son comparables con los del CI: el binario local no es el mismo (237 frente a 234 sobre un fichero intacto).

7. Estado

Fases 1 a 7 cerradas sobre el subconjunto seguro. La fase 8 es este documento. El goal no está completo: quedan los elementos de la sección 4, y los de riesgo alto requieren una decisión de prioridad y un plan de migración, no más ejecución a ciegas.