Inteligencia Artificial

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

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

Sloppy es un analizador estático para proyectos PHP, con reglas extra para 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 costo 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 más de veinte reglas (diez de ellas pensadas para Eloquent, que se activan cuando detecta Laravel 12 o 13), un modo de revisión sobre el diff de git, un baseline para proyectos que ya existen, hooks para Claude Code 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        # fuera de Laravel: vendor/bin/sloppy

Es un paquete joven: la primera versión, v0.1.0, salió en Packagist el 10 de septiembre de 2026 y la v1.0.0 el 25 de septiembre. 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 reglas

Estas son las 23 con las que salió la v0.1, y vienen en tres grupos. Después se sumaron SL111 (Copy-Paste Drift), SL112 (Placeholder Implementation, el // ... existing code ... que queda donde debería haber un cuerpo), tres reglas de supresión (SL501 a SL503: ignores sin motivo, baselines de PHPStan o Psalm que crecen y tests debilitados) y cinco opcionales (SL304 a SL308) que solo corren si describes la arquitectura de tu proyecto; el detalle está en la documentación de reglas. 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, y el paquete lo instala por ti: vendor/bin/sloppy agents install (o php artisan sloppy:agents) añade dos hooks a .claude/settings.json. Uno revisa cada edición de un archivo PHP y le pasa al modelo lo que esa edición introdujo; el otro, cuando el agente intenta terminar, lo devuelve a corregir los hallazgos nuevos que superan fail_on. La revisión ocurre antes de que tú leas el diff. Para que el agente lo llame a demanda hay además un servidor MCP (vendor/bin/sloppy-mcp), según la documentación de agentes. 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:fix, borra los comentarios que solo repiten su código y le pasa a Rector los hallazgos mecánicos.
  4. php artisan sloppy:baseline, aceptas la deuda de hoy y cierras la de mañana.
  5. '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(rule)   = Σ  weight(severity) × confidence / 100
units           = max(1, analysedLines / lines_per_unit)
coverage        = min(1, |⋃ affected lines| / analysedLines)
deduction(rule) = penalty(rule) / units × penalty_multiplier × (1 + coverage)
capped(rule)    = rule_cap × (1 − e^(−deduction(rule) / rule_cap))
score           = 100 − min(100, Σ capped(rule))

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, lines_per_unit y rule_cap son configurables bajo sloppy.score, según la documentación del score.

Tres cosas que la fórmula hace bien: un hallazgo del que solo está medio seguro cuesta la mitad, 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 rule_cap impide que una sola regla quite más de 30 puntos por mucho que se dispare.

Y una trampa que la propia documentación 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 405 líneas, escrita mal a propósito, puntúa 0.

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: 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 todas las reglas que corren siempre 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 el paquete no las presenta como certezas.

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 corre en cualquier proyecto PHP; con Laravel 12 o 13 suma las reglas de Eloquent y los comandos artisan. Por dentro se apoya en nikic/php-parser ^5.3 y en symfony/console, symfony/finder, symfony/process y symfony/yaml ^7 u ^8, y en Laravel el service provider se descubre solo. La configuración vive en un archivo PHP: en Laravel, config/sloppy.php, publicable con php artisan vendor:publish --tag=sloppy-config; fuera de Laravel, el binario usa el primero que exista entre config/sloppy.php, sloppy.php y .sloppy.php.

Es un proyecto joven. Las dos primeras versiones se publicaron en Packagist el 10 de septiembre de 2026 y la v1.0.0 llegó el 25 de septiembre. 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 99% de cobertura de líneas), pero sigue siendo un proyecto de un solo autor que nació en septiembre de 2026.

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 tan joven 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?

Sí. Corre en cualquier proyecto PHP 8.3 o superior con vendor/bin/sloppy, y encuentra las carpetas de código a partir del composer.json. Cuando detecta Laravel 12 o 13 suma diez reglas que entienden Eloquent y los comandos artisan.

¿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 todas sus 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