# Diff / Merge para operations.db y grafos

Gap identificado en el registry. Reimplementar desde cero.

Cuando un pipeline corre multiples veces, no hay forma de comparar resultados
entre ejecuciones. Necesario para detectar drift, validar estabilidad, y
mergear grafos de conocimiento de distintas fuentes.

## Funciones a implementar

### diff_entities

- **Dominio:** datascience
- **Lang:** Python
- **Purity:** pure
- **Signature:** `diff_entities(before: list[dict], after: list[dict], key: str = "id") -> dict`
- **Retorno:**
  ```python
  {
      "added": list[dict],       # entities nuevas (key en after, no en before)
      "removed": list[dict],     # entities eliminadas (key en before, no en after)
      "modified": list[dict],    # entities con cambios: {"key": str, "changes": {"field": {"old": ..., "new": ...}}}
      "unchanged": int,          # count de entities sin cambios
      "summary": str             # "3 added, 1 removed, 5 modified, 42 unchanged"
  }
  ```
- **Descripcion:** Compara dos snapshots de entities (tipicamente de dos ejecuciones del mismo pipeline). Detecta añadidas, eliminadas, y modificadas con detalle campo a campo. Ignora campos de metadata temporal (created_at, updated_at) por defecto.
- **Parametros opcionales:**
  - `ignore_fields: list[str] = ["created_at", "updated_at"]` — campos a excluir de la comparacion
  - `compare_fields: list[str] | None = None` — si se da, solo compara estos campos
- **Deps:** ninguna
- **Tests:**
  - Entity añadida
  - Entity eliminada
  - Entity modificada con detalle de campos
  - Entities identicas → unchanged
  - ignore_fields funciona
  - compare_fields filtra correctamente
  - Lista vacia vs lista con datos

### diff_relations

- **Dominio:** datascience
- **Lang:** Python
- **Purity:** pure
- **Signature:** `diff_relations(before: list[dict], after: list[dict], key: tuple[str, str, str] = ("source_id", "target_id", "relation_type")) -> dict`
- **Retorno:** Misma estructura que diff_entities pero con key compuesta
- **Descripcion:** Compara relaciones entre dos snapshots. Key compuesta porque las relaciones se identifican por (source, target, type), no por un solo ID.
- **Deps:** ninguna
- **Tests:**
  - Relacion añadida
  - Relacion eliminada
  - Relacion con metadata modificada (mismo source/target/type, distinto weight)
  - Key compuesta funciona correctamente

### detect_drift

- **Dominio:** datascience
- **Lang:** Python
- **Purity:** pure
- **Signature:** `detect_drift(history: list[dict], current: dict, fields: list[str], threshold: float = 2.0) -> list[dict]`
- **Retorno:** Lista de `{"field": str, "current": float, "mean": float, "std": float, "z_score": float, "drifted": bool}`
- **Descripcion:** Detecta drift estadistico comparando metricas de la ejecucion actual contra el historial. Usa z-score — si |z| > threshold, el campo ha drifteado. Pensado para comparar metrics de executions sucesivas en operations.db.
- **Ejemplo:**
  ```python
  history = [
      {"records_out": 100, "duration_ms": 500},
      {"records_out": 105, "duration_ms": 480},
      {"records_out": 98, "duration_ms": 510},
  ]
  current = {"records_out": 50, "duration_ms": 2000}
  detect_drift(history, current, ["records_out", "duration_ms"])
  # [{"field": "records_out", "current": 50, "mean": 101.0, "std": 3.6, "z_score": -14.2, "drifted": True},
  #  {"field": "duration_ms", "current": 2000, "mean": 496.7, "std": 15.3, "z_score": 98.3, "drifted": True}]
  ```
- **Deps:** `math` (solo stdlib)
- **Tests:**
  - Campo con drift claro (z > threshold)
  - Campo estable (z < threshold)
  - Historial con un solo punto → std=0, no puede calcular → drifted=False con nota
  - Historial vacio → todos drifted=False
  - Threshold custom

### merge_graphs

- **Dominio:** datascience
- **Lang:** Python
- **Purity:** pure
- **Signature:** `merge_graphs(graphs: list[dict], entity_key: str = "name", similarity_threshold: float = 0.85) -> dict`
- **Retorno:**
  ```python
  {
      "entities": list[dict],     # entities mergeadas (deduplicadas)
      "relations": list[dict],    # relaciones mergeadas
      "merge_log": list[dict]     # registro de merges: {"merged": [id1, id2], "into": id, "similarity": float}
  }
  ```
- **Descripcion:** Mergea multiples grafos de conocimiento (de distintas fuentes o ejecuciones) en uno solo. Deduplicacion de entities por similitud de nombre (Levenshtein normalizado). Relaciones se re-apuntan a las entities mergeadas. Atributos se combinan (union de campos, ultimo gana en conflictos).
- **Algoritmo:**
  1. Juntar todas las entities de todos los grafos
  2. Para cada par de entities con similitud de nombre >= threshold, mergear
  3. Elegir entity canonica (la que tiene mas campos no-null)
  4. Re-apuntar relaciones al ID canonico
  5. Deduplicar relaciones identicas (mismo source, target, type)
  6. Registrar cada merge en merge_log
- **Deps del registry:** `levenshtein_distance_py_cybersecurity` (o reimplementar inline)
- **Tests:**
  - Dos grafos con entity duplicada → merge
  - Entities similares pero bajo threshold → no merge
  - Relaciones re-apuntadas correctamente
  - Merge log registra cada merge
  - Tres grafos → merge transitivo
  - Grafos sin overlap → concatenacion simple

### diff_entities (Go)

- **Dominio:** datascience
- **Lang:** Go
- **Purity:** pure
- **Signature:** `DiffEntities(before, after []map[string]any, key string, ignoreFields []string) map[string]any`
- **Descripcion:** Misma semantica que Python. Retorno como map para serializacion JSON directa.
- **Tests:** Mismos casos que Python
