Qué es RealDiff y por qué importa
RealDiff es una herramienta open source (licencia MIT, publicada por issacnitin) que compara el comportamiento en tiempo de ejecución entre dos revisiones de Git. Mientras un git diff dice qué líneas se editaron, RealDiff instrumenta los tests en ambas revisiones, observa qué métodos se llaman, con qué argumentos y qué valores retornan, y reporta el primer cambio de comportamiento en cada árbol de llamadas.
El ejemplo canónico del propio repositorio es demoledor: sustituir List.Sort por OrderBy en un helper de infraestructura parece un refactor inocuo. List.Sort no es estable, OrderBy sí. Esa diferencia altera silenciosamente un motor de precios en un archivo que el pull request ni siquiera tocó: DiscountEngine.SelectDiscount pasa de devolver "CLEARANCE_40" a "SEASONAL_15" y CheckoutTotals.Compute salta de 60 a 85. Dos de los tres tests que ejecutaron ese código no tenían asserts que reaccionaran al cambio. Es exactamente el tipo de regresión silenciosa que un code review humano difícilmente detecta.
Cómo funciona la arquitectura
RealDiff no es un script: es un sistema con un motor de Rust y tracers por lenguaje, todos emitiendo el mismo contrato realdiff.trace/1 en NDJSON.
👥 ¿Quieres ir más allá de la noticia?
En nuestra comunidad discutimos las tendencias, compartimos oportunidades y nos ayudamos entre emprendedores. Sin humo, solo acción.
👥 Unirme a la comunidad- .NET 8: weaving de IL en tiempo de build con Mono.Cecil + xUnit + PDBs portables.
- Java: agente
java.lang.instrumentcon ASM sobre Maven/Gradle + JUnit/TestNG. - Node / TypeScript: hooks de
require(CommonJS) y loader ESM con Babel, más adaptadores para Jest/Vitest. - Go: reescritura AST estable dentro de un caché de build externo.
- Rust: reescritura estable con
syn/quoteen un caché SHA-256 externo. - Python 3.12+:
sys.monitoring(PEP 669) adjuntado al inicio del proceso, sin tocar bytecode ni AST.
El flujo es: resolver refs, crear worktrees aislados, construir e instrumentar, correr la base tres veces, correr la propuesta una vez, aprender las claves no deterministas, comparar, colapsar a la frontera de comportamiento y atribuir cada cambio a archivo editado o no editado.
La frontera de comportamiento, el concepto central
RealDiff no lista todos los métodos que cambiaron: los colapsa en su frontier, el miembro más bajo del call tree cuyos descendientes están sin cambios. Los llamadores modificados por encima son colateral y se suprimen. Lo que queda es la causa raíz.
En los seis pull requests de fixture de la release v0.4.0 (uno por lenguaje), todos con el mismo cambio de estabilidad de sort, el frontier siempre aparece en código de pricing no editado y el archivo modificado aporta cero miembros trazados:
| Lenguaje | Claves emparejadas | Colapso frontier |
|---|---|---|
| .NET | 319 | 9 → 3 (3.0x) |
| Node | 129 | 117 → 3 (39.0x) |
| Java | 132 | 117 → 3 (39.0x) |
| Go | 315 | 9 → 3 (3.0x) |
| Rust | 312 | 9 → 3 (3.0x) |
| Python | 310 | 6 → 3 (2.0x) |
Cada fixture tiene tres tests, modifica un único archivo de configuración y mantiene dos aserciones amplias pasando mientras solo una aserción exacta del ganador del empate reacciona. En cada run, al menos un call site queda sin cobertura de test: ahí es donde RealDiff añade señal que el code review no da.
Rendimiento medido y caché de baseline
El CLI implementa un caché opt-in con --cache-dir. En el caso de escala FluentValidation (99.000 eventos por run), un análisis en frío de cuatro runs tardó 339,705 s; con caché caliente, 53,962 s: una reducción del 84,1% (6,3x más rápido). El run cálido desglosa como 13,264 s de build, 2,729 s de weaving, 11,844 s de la única corrida instrumentada, 10,386 s de diff y 6,503 s de frontier.
Para runs retenidos en FluentValidation #2136, la ruta por defecto en Rust midió 6,626 s en frío y 7,331 s en caliente para diff + frontier compartido, con picos de 321/314 MiB de árbol de procesos. C# midió 13,685/14,037 s con picos de ~2,1 GiB. En pruebas de contenedor equivalentes, memory.current (cgroup-v2) alcanzó 2.661,461 MiB en frío y 2.524,680 MiB en caliente, lo que el propio proyecto documenta como base para provisionar capacidad con margen de carga.
Códigos de salida diseñados para no mentir
Los exit codes están pensados para CI, no para parecer limpios:
- 0: análisis completo, sin cambios inesperados.
- 1: hay hallazgos.
- 3: el sistema rehúsa dar veredicto por evidencia insuficiente (atribución de path, integridad del call tree o cobertura incompletas).
- 4: no se pudo instrumentar el repositorio.
- 5: el repositorio sin modificar no compiló en el entorno.
El código 3 es deliberadamente distinto de "limpio": un exit 3 obliga a investigar en vez de mergear un PR marcado falsamente como seguro.
Integración con GitHub Actions y Azure DevOps
El repositorio publica tres workflows: CI (build + pruebas ejecutables + empaquetado del CLI), Container (imagen única, pruebas de análisis en Node y Java con Docker como único prerequisito) y RealDiff blast radius (analiza PRs, sube findings.json y comenta en PRs del mismo repo).
Para adopción propia, basta copiar blastradius.yml y ajustar exclusiones de namespaces. El job requiere checkout con fetch-depth: 0 y el permiso pull-requests: write. En Azure Repos existe el equivalente en azure-pipelines.yml con la misma imagen multi-lenguaje; se agrega como política de rama Build validation porque YAML pr triggers no se honra en Azure Repos.
Estado actual y límites conocidos
El repositorio se describe como "early preview". El CLI unificado detecta repos .NET, Java (Maven/Gradle), Node (npm/pnpm/Yarn/Bun), Go modules, Cargo y Python 3.12+ a partir de marcadores raíz convencionales. Repositorios mixtos, monorepos o con múltiples entry points se rechazan en vez de adivinarse.
Límites explícitos por tracer: .NET excluye propiedades, eventos y operadores por política, y los inicializadores de tipo son estructuralmente inobservables (los hooks corren bajo el lock de inicialización del CLR y pueden causar deadlock). En Java, los inicializadores de clase corren bajo el lock del JVM con el mismo riesgo. En Node los workers están fuera de alcance; en Rust las macros, extern/const, uniones y trait objects son inalcanzables desde reescritura estable. Python rechaza versiones <3.12 y no implementa fallback a sys.settrace.
Qué significa esto para tu startup
RealDiff ataca un problema clásico de equipos en escala: el test coverage gap en código no editado. El refactor que rompe precios no está en el archivo que revisaste, así que pasa el code review. Si tus tests existentes no tienen asserts sobre el valor exacto en empates, el cambio se cuela. Esto es cada vez más relevante a medida que los agentes de IA generan PRs grandes donde el revisor humano no puede rastrear mentalmente cada call site.
Acciones concretas que puedes tomar esta semana:
- Habilita RealDiff en modo
warn-onlyen GitHub Actions copiandoblastradius.yml. Te dará señal sin romper merges hasta que valides que el ruido es aceptable en tu repo. - Activa el caché de baseline con
--cache-dirmontado en S3 o en el almacenamiento de CI. La diferencia entre 339 s y 54 s por PR cambia la conversación sobre adopción. - Convierte findings en una política versionada: usa
realdiff baseline writeparaAcknowledgement de 30 días de los miembros accionables actuales, de modo que migraciones intencionales no bloqueen PRs futuros con el mismo digest.
Fuentes
- RealDiff – issacnitin/RealDiff en GitHub
- BehaviorDiff – issacnitin/BehaviorDiff en GitHub
- Enumerable.OrderBy Method – Microsoft Learn
- Stable and Unstable Sorts in .NET – blog.ladeak.net
- How to Implement Visual Regression Testing in CI/CD – lastest.cloud
👥 ¿Quieres ir más allá de la noticia?
En nuestra comunidad discutimos las tendencias, compartimos oportunidades y nos ayudamos entre emprendedores. Sin humo, solo acción.
👥 Unirme a la comunidad














