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:
- Inventario de funcionalidades — qué existe hoy (1.021 capacidades).
- Inventario en JSON — la línea base comparable.
- Mapa de dependencias — quién llama a quién.
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:
- Dos ficheros gemelos que se van a desincronizar (
Admin_Status). - Cuatro copias a mano de los stubs de PHPStan de Core (coste de mantenimiento y falsos resultados en el análisis estático).
- 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.
- 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.
- Una dependencia no declarada:
assistantusaConvoca\CoresinRequires Plugins: convoca-core. - 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¶
python3 bin/convoca-inventario.py --checkantes de cada fusión. Falla si desaparece cualquier capacidad del baseline.- Las baterías de los tres sitios (demo, Lugg, biodevas) con 0 FAIL.
- Pantallas y flujos del inventario ejercitados tras cada despliegue.
- 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.