Inteligencia Artificial

Sloppy: análisis estático para el código que deja tu agente

Autorangel cruz
Publicado
Lectura16 min de lectura
Sloppy: análisis estático para el código que deja tu agente

Sloppy es un analizador estático para proyectos Laravel que busca los patrones de mala calidad que dejan atrás los agentes de código. Analiza tu PHP con un parser real en lugar de expresiones regulares, y reporta las formas que se convierten en coste de mantenimiento: métodos dios, excepciones tragadas, N+1 probables, lógica de negocio dentro de controladores y abstracciones que nunca se ganaron el sueldo. Trae 23 reglas, un modo de revisión sobre el diff de git, un baseline para proyectos que ya existen y salida JSON para CI. Es determinista, corre en tu máquina y no necesita modelo, API key ni red.

composer require --dev heyosseus/sloppy
php artisan sloppy

Es un paquete muy joven: la versión v0.1.1 se publicó en Packagist el 10 de septiembre de 2026, y es MIT. Eso importa para decidir si lo metes hoy en un pipeline de producción, y volveré sobre ello al final.

El hueco que ningún linter estaba cubriendo

Si ya corres PHPStan y Pint en tu proyecto Laravel, tienes cubiertas dos preguntas: ¿esto es correcto de tipos? y ¿esto está formateado igual que el resto?. Los tests responden una tercera: ¿esto se comporta como debe?.

Cuando un agente te devuelve 400 líneas en treinta segundos, la pregunta que importa es una cuarta: ¿esto tiene forma de código del que alguien se va a arrepentir?

Una acción de controlador de 200 líneas que escribe en cuatro tablas, llama a una API de pagos y se traga un Throwable pasa PHPStan sin una queja. Pint la deja preciosa. Y los tests, si los hay, pueden estar todos en verde, que es un problema que ya me encontré de frente cuando seis bugs se colaron entre 615 aserciones. El código está tipado, formateado y probado, y sigue siendo deuda técnica recién nacida.

El propio README lo resume en una tabla:

Herramienta Qué responde
PHPStan / Psalm ¿Es correcto de tipos?
Pint / PHP_CodeSniffer ¿Está formateado consistentemente?
Pest / PHPUnit ¿Se comporta bien?
Rector ¿Se puede transformar mecánicamente?
Sloppy ¿Tiene forma de código del que alguien se va a arrepentir?

La regla de diseño del autor es explícita: si PHPStan puede demostrarlo, Sloppy no se mete.

No es un detector de IA

Sloppy no intenta adivinar quién escribió una línea. La postura del README es que nadie puede demostrar autoría de forma fiable desde el código fuente, y que una herramienta que lo prometiera te estaría vendiendo un lanzamiento de moneda con una barra de progreso. Lo comparto: la mitad de los proyectos que salieron el último año a "detectar código de IA" son eso.

Lo que detecta es slop: patrones que correlacionan con salida rápida y sin revisar, y con deuda técnica en general. Los números que ves nunca hablan del autor:

  • Confidence: 88% es cuánta seguridad tiene el analizador de que el patrón que describe está realmente ahí. No es una probabilidad de que lo escribiera una IA.
  • Score: 67/100 es una medida de riesgo de calidad sobre las rutas analizadas. No es "el 67% de esto lo generó una IA".
  • SL107 Swallowed Exception dice que ese catch no hace nada observable con el fallo. No acusa a nadie.

Esa distinción es la que hace que la herramienta sea usable en un equipo. Una que dijera "esto lo escribió Claude" abre una conversación que no lleva a ningún sitio; una que dice "esta acción escribe en cuatro tablas sin transacción" abre la que sirve.

Las 23 reglas de la v0.1

Vienen en tres grupos. Las de arquitectura son explícitamente consultivas: dicen que una abstracción no se está ganando el sueldo para el uso actual, que es un juicio sobre el código de hoy y no una prohibición de usar repositorios o interfaces.

PHP y generales

ID Regla Severidad Qué busca
SL101 God Method Alta Métodos excesivamente largos, ramificados o habladores, medido por varias vías independientes.
SL102 God Class Alta Clases grandes en varias dimensiones a la vez: tamaño, número de métodos, dependencias inyectadas y colaboradores.
SL103 Excessive Nesting Media Condicionales, bucles y bloques try anidados más allá del límite configurado.
SL104 Duplicate Logic Media Métodos cuyo cuerpo es estructuralmente idéntico al de otro método del proyecto.
SL105 Dead Private Method Media Métodos privados sin ninguna llamada visible en el archivo que los declara.
SL106 Unused Constructor Dependency Media Dependencias inyectadas por constructor que nunca se leen en la clase.
SL107 Swallowed Exception Alta Bloques catch que ni relanzan, ni reportan, ni registran, ni reaccionan al fallo.
SL108 Redundant Condition Baja Condiciones repetidas dentro de sí mismas, duplicadas en una cadena if/elseif o escritas como un literal.
SL109 Narrative Comment Baja Comentarios cuyas palabras ya aparecen todas en la línea que tienen debajo.
SL110 Defensive Programming Noise Baja Un método que protege el mismo sujeto dos veces igual, con el mismo resultado y sin reasignación en medio.

SL109 merece una mención aparte, porque es el tic más reconocible del código generado sin revisar: el // Obtener el usuario encima de $user = User::find($id);.

Laravel

ID Regla Severidad Qué busca
SL201 Business Logic In Controller Alta Acciones que combinan varias preocupaciones de negocio: escrituras, cálculos, transacciones, llamadas salientes y ramificación.
SL202 Inline Validation Baja Conjuntos grandes de reglas de validación en línea, donde un FormRequest las llevaría mejor.
SL203 Possible N+1 Media Acceso a una relación dentro de un bucle cuando la colección no parece cargarla con eager loading.
SL204 Query Inside Loop Media Consultas de Eloquent o del query builder ejecutadas dentro de un bucle.
SL205 Collection Instead Of Database Query Media Cargar una tabla entera y a continuación hacer con la colección lo que podía hacer la base de datos.
SL206 Excessive Controller Dependencies Media Controladores que inyectan más colaboradores de los configurados.
SL207 Excessive Service Dependencies Media Lo mismo para clases de servicio, acciones y managers.
SL208 Direct External API Call Media Peticiones HTTP salientes hechas directamente desde controladores, modelos, form requests o middleware.
SL209 Model Doing Too Much Media Modelos Eloquent que hacen llamadas salientes, despachan notificaciones o jobs, o cargan flujos largos de negocio.
SL210 Suspicious Model::all() Media Un Model::all() que se itera en el mismo método, o que se llama dentro de un bucle.

Arquitectura (consultivas)

ID Regla Severidad Qué busca
SL301 Abstraction Inflation Media Conceptos envueltos en varias capas donde al menos una es trivial, tiene una sola implementación o un solo uso.
SL302 Empty Wrapper Class Media Clases cuyos métodos públicos casi todos reenvían sus argumentos sin tocarlos a un único colaborador inyectado.
SL303 Single-Use Abstraction Baja Interfaces y clases abstractas pequeñas con exactamente una implementación y como mucho un archivo que las llama.

SL301 es la que más me interesa de las tres, porque la inflación de abstracciones es lo que produce un agente al que le pides "hazlo bien": te devuelve una interfaz, una implementación, un contrato y una factoría para algo que se llama desde un sitio. Por eso la regla nunca reporta por encima del 78% de confianza: es una heurística y el paquete lo admite.

sloppy:diff, o revisar solo lo que cambió

Escanear el proyecto entero está bien la primera vez. Lo que encaja con trabajar con agentes es revisar lo que acaba de cambiar:

php artisan sloppy:diff             # árbol de trabajo contra HEAD
php artisan sloppy:diff HEAD~1      # contra el commit anterior
php artisan sloppy:diff main        # todo lo que cambió esta rama

Separa lo que tu cambio introdujo de lo que simplemente heredó, y solo los hallazgos nuevos pueden tumbar el build. Tres detalles de implementación explican por qué eso no se vuelve ruido:

  • Revisa el árbol de trabajo, no solo los commits. Los archivos sin commitear y sin trackear entran en el análisis, así que la clase que el agente acaba de escribir se revisa antes de commitear.
  • Los hallazgos se emparejan por huella y no por número de línea, así que añadir un use arriba del archivo no convierte en nuevos todos los hallazgos que ya había.
  • Las reglas que cruzan archivos siguen viendo el proyecto entero. Solo se ejecutan sobre los archivos cambiados, pero el índice se construye con todo, así que SL303 puede saber que una interfaz tiene una sola implementación aunque ese archivo no esté en el diff.

Si ya tienes el flujo de Claude Code en un proyecto Laravel montado, esto se cuelga de un hook de Claude Code sin inventar nada: el agente termina, el hook corre sloppy:diff, y la revisión ocurre antes de que tú leas el diff. El roadmap del proyecto contempla además un servidor MCP para que el agente revise su propio trabajo antes de devolvértelo. Si quieres el contexto de por qué MCP en Laravel es el sitio natural para eso, ahí lo tienes.

Adoptarlo sin tener que arreglar el pasado primero

El motivo por el que la mayoría de los analizadores acaban desactivados es la primera ejecución: 900 hallazgos, nadie los va a arreglar, se comenta la línea del CI. Sloppy trae la salida estándar para eso:

php artisan sloppy:baseline

Registra lo que hay hoy en .sloppy-baseline.json, lo commiteas, y a partir de ahí el gate aplica solo a lo que venga. Las entradas del baseline se indexan por regla, archivo y una huella que aporta la propia regla, normalmente clase y miembro, nunca por número de línea, así que el baseline sobrevive a la edición normal del archivo. Si un hallazgo aparece más veces de las que registró el baseline, las extras cuentan como nuevas.

Para una primera pasada ruidosa, el orden recomendado va de lo fino a lo bruto:

  1. min_confidence: 75, y te quedas solo con lo que el analizador ve claro.
  2. 'SL109' => ['severity' => 'info'], el hallazgo sigue visible pero deja de importar.
  3. php artisan sloppy:baseline, aceptas la deuda de hoy y cierras la de mañana.
  4. 'SL109' => ['enabled' => false], último recurso.

El slop score, y por qué no hay que tomárselo como una nota

El comando devuelve un número único y determinista para las rutas analizadas:

penalty  = Σ  weight(severity) × confidence / 100
units    = max(1, analysedLines / lines_per_unit)
density  = penalty / units
coverage = min(1, Σ affectedLines / analysedLines)
score    = 100 − min(100, density × penalty_multiplier × (1 + coverage))

Los pesos por defecto son critical: 20, high: 10, medium: 4, low: 1.5, info: 0.5, y las bandas van de 0 a 39 (severe slop), 40 a 59 (sloppy), 60 a 74 (needs attention), 75 a 89 (healthy) y 90 a 100 (clean). Todo eso, el multiplicador y lines_per_unit son configurables bajo sloppy.score.

Dos cosas que la fórmula hace bien: un hallazgo del que solo está medio seguro cuesta la mitad, y el tamaño del proyecto se normaliza, así que una aplicación grande no recibe un castigo por el mero hecho de ser grande.

Y una trampa que el propio README señala: como el score es una densidad, una base de código pequeña y llena de hallazgos toca fondo enseguida. La aplicación de demostración de 472 líneas, escrita mal a propósito, puntúa 0. Eso es la aritmética funcionando, no un veredicto.

Mi lectura práctica: usa el score para ver la tendencia entre ejecuciones, y el score.delta del modo diff para juzgar un cambio concreto. Como KPI absoluto para un dashboard no sirve, y el autor tampoco pretende que sirva.

¿No se queja de todo?

Es el modo de fallo de cualquier analizador, y el proyecto responde con una medida en vez de con una promesa. Corre su propio conjunto de reglas sobre su propio código en CI, y ahí aparece un solo hallazgo, que además es real: NodeHelper es una clase grande. La decisión de no partirla en cinco está registrada en un test con su razonamiento, porque hacerlo sería la ceremonia que el paquete existe para desalentar. Ese test falla ante cualquier hallazgo nuevo sobre el código de Sloppy.

La otra mitad de la respuesta es tests/Fixtures/Good: código Laravel deliberadamente normal, con un test que afirma que las 23 reglas reportan cero hallazgos sobre él, con score 100. Si un cambio en cualquier regla rompe ese test, la regla está mal.

A eso se suman cuatro decisiones de diseño contra los falsos positivos: señales múltiples antes de disparar (SL101 y SL102 necesitan varias medidas por encima del límite, no una), lenguaje cauto cuando la certeza es imposible ("posible N+1"), conciencia del framework (relaciones, scopes, accessors, casts y boot() son para lo que existen los modelos y nunca cuentan en contra), y salidas de emergencia por todas partes: métodos mágicos, acceso dinámico a propiedades, atributos, reflexión, compact() o subclases hacen que la regla se eche atrás en vez de adivinar.

Además, ninguna regla reporta nunca un 100% de confianza. Son heurísticas, y una heurística que se declara certera está mintiendo.

CI y códigos de salida

Lo que tiene sentido en un pull request es cerrar el paso a lo que el cambio introduce, no a lo que heredó:

name: sloppy
 
on: pull_request
 
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          # El modo diff necesita el commit base, así que un clone shallow no basta.
          fetch-depth: 0
 
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
 
      - run: composer install --no-interaction --prefer-dist
 
      - name: Review the change
        run: php artisan sloppy:diff origin/${{ github.base_ref }}

Los códigos de salida son tres:

Código Significado
0 El análisis terminó y nada superó fail_on
1 El análisis terminó y encontró algo en o por encima de fail_on
2 El análisis no pudo correr: configuración mala, sin repositorio git, baseline ilegible

La distinción entre 1 y 2 es la que se le olvida a casi todo el mundo. Un pipeline que los confunde o ignora hallazgos reales, o falla por una errata en el config sin decirte cuál de las dos cosas pasó.

La salida JSON (--format=json) es un contrato publicado: el campo schema sube cuando un campo cambia de significado, y dentro de una versión del esquema las claves solo se añaden. Los hallazgos salen en orden canónico, así que dos ejecuciones sobre el mismo código producen bytes idénticos, que es lo que lo hace diffeable en CI.

Requisitos y estado real del proyecto

Pide PHP 8.3 o superior y Laravel 12 o 13. Por dentro se apoya en nikic/php-parser ^5.3 y en symfony/finder y symfony/process ^7 u ^8, y el service provider se descubre solo. La configuración vive entera en config/sloppy.php, publicable con php artisan vendor:publish --tag=sloppy-config, y no hay un segundo formato de configuración que mantener sincronizado.

Conviene decirlo con claridad: esto es una v0.1. El repositorio se creó el 10 de septiembre de 2026 y las dos primeras versiones se publicaron ese mismo día. El paquete tiene una calidad de ejecución muy por encima de lo normal para su edad (suite con Rector, Pint, PHPStan en nivel 8, 100% de cobertura de tipos y un suelo del 95% de cobertura de líneas), pero sigue siendo un proyecto de un solo autor con días de vida.

Lo que yo haría con eso: instálalo como --dev, córrelo en local con sloppy:diff sobre lo que te devuelve el agente, y deja pasar unas semanas antes de ponerlo a tumbar builds en main. Con fail_on en null ves los hallazgos sin arriesgar el pipeline, que es el punto medio razonable.

Dónde encaja

La revisión de código dejó de escalar en el momento en que un agente escribe más rápido de lo que tú lees, y el catálogo de agentes sigue creciendo, desde Claude Code hasta cosas como fx, el agente de Vercel escrito en Zig. Para eso sirve mover parte de la revisión a algo determinista que corra antes que tú.

Esa es la diferencia entre vibe coding y agentic engineering: la marca el proceso que hay alrededor de la herramienta. Y conecta con algo que ya medí cuando comparé PHP con los criterios de Google sobre qué hace a un lenguaje bueno para revisar código de agentes: las herramientas de análisis que corren sin desplegar nada son el punto donde PHP compite bien.

Sloppy no es la única forma de cerrar ese hueco, y siendo una v0.1 tampoco es la apuesta segura. Es el primer paquete que he visto plantear esa pregunta en el ecosistema Laravel, y la plantea sin venderte un detector de IA que no puede existir.

Preguntas frecuentes

¿Sloppy detecta si mi código lo escribió una IA?

No, y el proyecto es explícito en que nadie puede hacerlo de forma fiable desde el código fuente. Detecta patrones que correlacionan con salida rápida y sin revisar, que también aparecen en código escrito por personas con prisa. El Confidence: 88% mide cuánta seguridad hay de que el patrón esté presente, no de quién lo escribió.

¿Sustituye a PHPStan?

No, y está diseñado para no solaparse. PHPStan responde si el código es correcto de tipos; Sloppy, si tiene forma de generar deuda. La regla del autor es que si PHPStan puede demostrarlo, Sloppy no se mete. Corre los dos.

¿Funciona fuera de Laravel?

La v0.1 es un paquete de Laravel: se instala como service provider y se usa mediante comandos artisan. Necesita Laravel 12 o 13. De las 23 reglas, 13 son de PHP general o de arquitectura y aplicarían a cualquier proyecto, pero hoy el envoltorio es el framework.

¿Necesita API key o manda mi código a algún sitio?

No. Es determinista y local, sin modelo, sin API key y sin red. El mismo código produce siempre el mismo informe, que es justo lo que lo hace usable como gate de CI. El roadmap contempla asistencia con IA, pero declarada como opcional, separada y nunca necesaria para obtener un informe.

¿Cómo lo activo en un proyecto viejo sin ahogarme en avisos?

Con php artisan sloppy:baseline. Registra la deuda de hoy en .sloppy-baseline.json, lo commiteas, y el gate pasa a aplicar solo a lo que venga después. Como el baseline se indexa por huella de clase y miembro en vez de por número de línea, sobrevive a la edición normal de los archivos.

¿Qué hago si me marca un falso positivo?

Reportarlo. El proyecto lo trata como un bug, no como un umbral que haya que esquivar, y mantiene un test de regresión sobre código Laravel normal que exige cero hallazgos de las 23 reglas. Si necesitas silenciarlo mientras tanto, baja la severidad antes que desactivar la regla: el hallazgo sigue visible sin tumbar el build.

Fuentes

Las traducciones de las tablas de reglas y de comparación con otras herramientas son mías, a partir del README del proyecto.

¿Tienes un proyecto en mente?

Trabajo con Laravel, WordPress, SEO técnico y servidores MCP. El primer paso es una llamada de descubrimiento, sin costo ni compromiso, donde me cuentas qué necesitas y te digo con honestidad si puedo ayudarte.

Hablemos