Skip to content

05_02_06_diff_visualization - Visualización de Diferencias Temporales

🔀 Propósito

El Diff Visualization compara grafos entre commits/versions mostrando qué cambió: módulos añadidos, removidos, re-cableados. Responde "¿Cómo evolucionó la arquitectura?" y detecta cambios arquitectónicos significativos.

🏗️ Arquitectura

05_02_06_diff_visualization/
├── graph_diff.hpp          # Motor de diff
├── test_diff.cpp          # Tests
└── README.md             # Documentación

Clases Implementadas

  1. GraphDiffCalculator - Detección de cambios
  2. DiffVisualizer - Renderización visual
  3. GitDiffIntegration - Integración con git

📊 Tipos de Cambios Detectables

Node Changes

  • ADDED ➕ - Módulo nuevo
  • REMOVED ➖ - Módulo eliminado
  • MODIFIED 📝 - Propiedades cambiadas
  • UNCHANGED - Sin cambios

Edge Changes

  • ADDED ➕ - Nueva dependencia
  • REMOVED ➖ - Dependencia eliminada
  • MODIFIED 📝 - Tipo/versión cambiada

Property Changes (MODIFIED)

  • label - Nombre del módulo
  • level - Cambio de jerarquía (⚠️ grave)
  • category - Cambio de categoría
  • status - Estado de desarrollo
  • cpu_cycles - Cambio significativo (>20%)
  • latency_samples - Latencia modificada

🔍 Uso Básico

Comparar Dos Grafos

#include "graph_diff.hpp"

using namespace audio_lab::dependency_graph::diff;

// Load graphs
Graph old_graph = load_from_catalog("catalog_v1.json");
Graph new_graph = load_from_catalog("catalog_v2.json");

// Calculate diff
auto diff = GraphDiffCalculator::calculate(
    old_graph,
    new_graph,
    "v1.5.0",  // old version
    "v1.6.0"   // new version
);

// Summary report
std::cout << diff.summary();

Output:

Graph Diff: v1.5.0 → v1.6.0

Node Changes:
  ➕ Added: 5
  ➖ Removed: 2
  📝 Modified: 8

Edge Changes:
  ➕ Added: 12
  ➖ Removed: 3
  📝 Modified: 1

Total changes: 31

Detailed Report

std::cout << diff.detailed_report();

Output:

Graph Diff: v1.5.0 → v1.6.0

Node Changes:
  ➕ Added: 5
  ➖ Removed: 2
  📝 Modified: 8

Nodes Added:
  ➕ ADDED: new_filter_module (L1_ATOM)
  ➕ ADDED: experimental_osc (L1_ATOM)
  ➕ ADDED: advanced_reverb (L2_CELL)
  ...

Nodes Removed:
  ➖ REMOVED: legacy_filter (L1_ATOM)
  ➖ REMOVED: old_delay (L1_ATOM)

Nodes Modified:
  📝 MODIFIED: svf_filter
     cpu_cycles: 120 → 95
     status: STABLE → STABLE
  📝 MODIFIED: poly_synth
     level: L2_CELL → L3_ENGINE
     cpu_cycles: 1800 → 2000

Edge Changes:
  ➕ EDGE ADDED: new_filter → add_kernel
  ➕ EDGE ADDED: advanced_reverb → svf_filter
  ➖ EDGE REMOVED: legacy_filter → mul_kernel
  📝 EDGE MODIFIED: poly_synth → reverb_cell

📈 Métricas de Cambio

Churn Rate

Definición: Tasa de inestabilidad arquitectónica

Fórmula:

churn_rate = (nodes_added + nodes_removed) / total_nodes

Uso:

size_t total = old_graph.get_nodes().size();
double churn = diff.calculate_churn_rate(total);

std::cout << "Churn rate: " << (churn * 100) << "%\n";

Interpretación: - < 5% = Estable, cambios incrementales - 5-15% = Evolución normal - 15-30% = Refactoring significativo - > 30% = Arquitectura inestable

Dependency Volatility

Definición: Cambios en el cableado del grafo

Fórmula:

volatility = (edges_added + edges_removed) / total_edges

Interpretación: - < 10% = Cableado estable - 10-25% = Re-cableado moderado - > 25% = Reorganización arquitectónica

Impact Radius

Definición: Módulos afectados indirectamente

size_t impact_radius = 0;
for (const auto& change : diff.node_changes) {
    if (change.type == ChangeType::MODIFIED) {
        // Count dependents affected
        auto dependents = old_graph.get_dependents(change.node_id);
        impact_radius += dependents.size();
    }
}

🎨 Modos de Visualización

1. Side-by-Side

Propósito: Comparar estados antes/después

auto dot = DiffVisualizer::generate_side_by_side_dot(
    old_graph,
    new_graph,
    diff
);

std::ofstream file("diff_side_by_side.dot");
file << dot;
file.close();

// Generate image
system("dot -Tsvg diff_side_by_side.dot -o diff.svg");

Output visual:

┌──────────────┐      ┌──────────────┐
│   v1.5.0     │      │   v1.6.0     │
├──────────────┤      ├──────────────┤
│ [A] ─→ [B]   │      │ [A] ─→ [B]   │
│ [C] (red)    │      │ [D] (green)  │
│ [E] ─→ [F]   │      │ [E] ─→ [F]   │
└──────────────┘      └──────────────┘

Legend:
  Red    = Removed
  Green  = Added
  Orange = Modified
  Gray   = Unchanged

2. Overlay

Propósito: Ver cambios en contexto del grafo nuevo

auto dot = DiffVisualizer::generate_overlay_dot(new_graph, diff);

std::ofstream file("diff_overlay.dot");
file << dot;
file.close();

Colorización: - 🟢 Verde = Nodos/aristas añadidos - 🔴 Rojo = Removidos (no visible, solo en old) - 🟠 Naranja = Modificados - ⚪ Gris = Sin cambios

Uso: Focus en cambios recientes, contexto del grafo actual

3. Animation (Concepto)

Propósito: Transición animada A → B

// D3.js animation concept
nodes.transition()
    .duration(1000)
    .attr("cx", d => d.new_x)
    .attr("cy", d => d.new_y)
    .style("fill", d => {
        if (d.change === "ADDED") return "green";
        if (d.change === "REMOVED") return "red";
        if (d.change === "MODIFIED") return "orange";
        return "gray";
    });

🔗 Git Integration

Compare Commits

#include "graph_diff.hpp"

auto diff = GitDiffIntegration::compare_commits(
    "/path/to/repo",
    "abc123f",     // old commit
    "def456a",     // new commit
    "catalog.json" // catalog path in repo
);

std::cout << diff.summary();

Internamente ejecuta:

git show abc123f:catalog.json > /tmp/old.json
git show def456a:catalog.json > /tmp/new.json
# Compare graphs

Compare with HEAD

auto diff = GitDiffIntegration::compare_with_head(
    "/path/to/repo",
    "catalog.json"
);

if (diff.has_changes()) {
    std::cout << "⚠️  Uncommitted architectural changes:\n";
    std::cout << diff.detailed_report();
}

Uso: Pre-commit hook para detectar cambios arquitectónicos

Compare Branches

# Via git tags
git show v1.5.0:catalog.json > old.json
git show v1.6.0:catalog.json > new.json

# Compare programmatically
./graph-diff old.json new.json

📊 Casos de Uso Prácticos

Caso 1: Release Notes Automation

// Generate architectural changes for release notes
auto diff = GitDiffIntegration::compare_commits(
    repo_path,
    "v1.5.0",
    "v1.6.0",
    "catalog.json"
);

std::cout << "# Architecture Changes v1.6.0\n\n";

if (diff.nodes_added > 0) {
    std::cout << "## New Modules\n";
    for (const auto& change : diff.node_changes) {
        if (change.type == ChangeType::ADDED) {
            std::cout << "- **" << change.new_node->label << "** ("
                      << to_string(change.new_node->level) << ")\n";
        }
    }
    std::cout << "\n";
}

if (diff.nodes_removed > 0) {
    std::cout << "## Deprecated Modules\n";
    for (const auto& change : diff.node_changes) {
        if (change.type == ChangeType::REMOVED) {
            std::cout << "- ~~" << change.old_node->label << "~~\n";
        }
    }
    std::cout << "\n";
}

std::cout << "## Performance Improvements\n";
for (const auto& change : diff.node_changes) {
    if (change.type == ChangeType::MODIFIED) {
        for (const auto& prop : change.property_changes) {
            if (prop.property_name == "cpu_cycles") {
                int old_cpu = std::stoi(prop.old_value);
                int new_cpu = std::stoi(prop.new_value);
                if (new_cpu < old_cpu) {
                    double improvement = ((old_cpu - new_cpu) / (double)old_cpu) * 100;
                    std::cout << "- " << change.new_node->label
                              << ": " << improvement << "% faster\n";
                }
            }
        }
    }
}

Caso 2: Breaking Change Detection

// Detect breaking architectural changes
auto diff = GitDiffIntegration::compare_with_head(repo_path);

std::vector<std::string> breaking_changes;

for (const auto& change : diff.node_changes) {
    // Level change = breaking
    if (change.type == ChangeType::MODIFIED) {
        for (const auto& prop : change.property_changes) {
            if (prop.property_name == "level") {
                breaking_changes.push_back(
                    "⚠️  BREAKING: " + change.node_id +
                    " changed level from " + prop.old_value +
                    " to " + prop.new_value
                );
            }
        }
    }

    // Node removed = potentially breaking
    if (change.type == ChangeType::REMOVED) {
        auto dependents = old_graph.get_dependents(change.node_id);
        if (!dependents.empty()) {
            breaking_changes.push_back(
                "⚠️  BREAKING: Removed " + change.node_id +
                " used by " + std::to_string(dependents.size()) + " modules"
            );
        }
    }
}

if (!breaking_changes.empty()) {
    std::cout << "🔴 BREAKING CHANGES DETECTED:\n";
    for (const auto& bc : breaking_changes) {
        std::cout << bc << "\n";
    }
    exit(1);  // Fail CI
}

Caso 3: Architecture Evolution Report

// Monthly architecture review
struct ArchEvolution {
    std::string version;
    double churn_rate;
    double volatility;
    size_t complexity_delta;
};

std::vector<ArchEvolution> evolution;

// Compare sequential versions
std::vector<std::string> versions = {"v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0"};

for (size_t i = 1; i < versions.size(); ++i) {
    auto diff = GitDiffIntegration::compare_commits(
        repo_path,
        versions[i-1],
        versions[i]
    );

    Graph old_g = load_graph(versions[i-1]);
    auto old_stats = old_g.compute_statistics();

    ArchEvolution ev;
    ev.version = versions[i];
    ev.churn_rate = diff.calculate_churn_rate(old_stats.node_count);
    ev.volatility = diff.calculate_dependency_volatility(old_stats.edge_count);
    ev.complexity_delta = diff.nodes_added - diff.nodes_removed;

    evolution.push_back(ev);
}

// Report
std::cout << "Architecture Evolution Trend:\n\n";
std::cout << "Version  | Churn | Volatility | Complexity Δ\n";
std::cout << "---------|-------|------------|-------------\n";

for (const auto& ev : evolution) {
    std::cout << ev.version << " | "
              << std::fixed << std::setprecision(2)
              << (ev.churn_rate * 100) << "% | "
              << (ev.volatility * 100) << "% | "
              << (ev.complexity_delta > 0 ? "+" : "")
              << ev.complexity_delta << "\n";
}

Caso 4: Refactoring Impact Visualization

// Before refactoring: save baseline
Graph baseline = current_graph;

// ... do refactoring ...

// After refactoring: compare
Graph refactored = current_graph;

auto diff = GraphDiffCalculator::calculate(
    baseline,
    refactored,
    "Before Refactoring",
    "After Refactoring"
);

std::cout << "Refactoring Impact:\n";
std::cout << diff.summary();

// Visualize changes
auto overlay_dot = DiffVisualizer::generate_overlay_dot(refactored, diff);
render_to_svg(overlay_dot, "refactoring_impact.svg");

// Metrics
double churn = diff.calculate_churn_rate(baseline.get_nodes().size());

if (churn > 0.15) {
    std::cout << "⚠️  High churn rate: " << (churn * 100) << "%\n";
    std::cout << "   Consider smaller refactoring steps\n";
} else {
    std::cout << "✅ Refactoring impact acceptable: " << (churn * 100) << "%\n";
}

🧪 Testing

TEST_CASE("Detect node addition") {
    Graph old_g, new_g;

    // Old: A, B
    add_nodes(old_g, {"A", "B"});

    // New: A, B, C
    add_nodes(new_g, {"A", "B", "C"});

    auto diff = GraphDiffCalculator::calculate(old_g, new_g);

    REQUIRE(diff.nodes_added == 1);
    REQUIRE(diff.node_changes[0].node_id == "C");
    REQUIRE(diff.node_changes[0].type == ChangeType::ADDED);
}

TEST_CASE("Detect property modification") {
    Graph old_g, new_g;

    auto old_node = std::make_shared<Node>("A", "Module A", L1_ATOM);
    old_node->cpu_cycles = 100;
    old_g.add_node(old_node);

    auto new_node = std::make_shared<Node>("A", "Module A", L1_ATOM);
    new_node->cpu_cycles = 150;  // 50% increase
    new_g.add_node(new_node);

    auto diff = GraphDiffCalculator::calculate(old_g, new_g);

    REQUIRE(diff.nodes_modified == 1);
    REQUIRE(diff.node_changes[0].has_property_changes());
}

TEST_CASE("Churn rate calculation") {
    Graph old_g, new_g;

    // Old: 100 nodes
    for (int i = 0; i < 100; ++i) {
        add_node(old_g, "node_" + std::to_string(i));
    }

    // New: 95 old + 10 new = 105 total
    // Removed: 5, Added: 10
    for (int i = 0; i < 95; ++i) {
        add_node(new_g, "node_" + std::to_string(i));
    }
    for (int i = 100; i < 110; ++i) {
        add_node(new_g, "node_" + std::to_string(i));
    }

    auto diff = GraphDiffCalculator::calculate(old_g, new_g);

    // Churn = (10 added + 5 removed) / 100 old = 0.15 = 15%
    REQUIRE(diff.calculate_churn_rate(100) == Approx(0.15));
}

📈 Complejidad

Operación Complejidad Notas
Node diff O(V) Scan ambos grafos
Edge diff O(E) Build edge maps
Property comparison O(1) Por nodo
Visualization O(V+E) Render completo
Git integration O(V+E) + I/O Load + compare

🚀 Próximos Pasos

Implementado: - Change detection (nodes/edges/properties) - Diff algorithms - 3 modos de visualización - Métricas de cambio - Git integration

Futuras Mejoras: - Semantic diff (understanding meaningful changes) - Animation support (web UI) - Diff optimization (incremental) - Historical trend analysis - AI-powered change suggestions


Status: ✅ Diff Visualization Completo Change types: 3 (ADDED/REMOVED/MODIFIED) Visualizations: 3 modos (side-by-side/overlay/animation concept) Metrics: Churn rate, volatility, impact radius Git: Full integration