---
id: "0049g"
title: "`graph_sources`: lector de `operations.db` + abstraccion funcional"
status: completado
type: feature
domain: []
scope: multi-app
priority: alta
depends: []
blocks: []
related: []
created: 2026-05-17
updated: 2026-05-17
tags: []
---
# 0049g — `graph_sources`: lector de `operations.db` + abstraccion funcional

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | 0049g |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature — parte de [#0049](0049-osint-graph-viewer.md) |

## Dependencias

**Bloqueada por:** [0049e](0049e-graph-types-extended.md) (necesita `EntityType`/`RelationType`).

---

## Objetivo

Crear la funcion `graph_sources` con la abstraccion `GraphLoadFn` y la primera implementacion: `graph_load_from_operations`. Diseñada para que JSON/JSONL/GraphML se anadan despues sin tocar el resto del codigo. Incluye variante streaming para "recoleccion masiva".

## Contexto

`operations.db` es la BD de cada app del registry con `entities`, `relations`, `executions`, `assertions`. Schema relevante:

```sql
entities    (id TEXT PK, type TEXT, status TEXT, metadata JSON, created_at, updated_at)
relations   (id TEXT PK, source TEXT, target TEXT, type TEXT, status TEXT, weight REAL, metadata JSON, ...)
```

Mapeo a `GraphData`:
- Cada valor distinto de `entities.type` → un `EntityType` (color generado por hash, shape default `circle`, icon 0). El consumer puede sobreescribir via `types.yaml` (lo hace la app `graph_explorer` en 0049k).
- Cada valor distinto de `relations.type` → un `RelationType`.
- Cada `entity` → un `GraphNode` con `user_data = hash64(entity.id)` y `label_idx` apuntando a string pool con `entity.id` o `metadata.name` si existe.
- Cada `relation` → un `GraphEdge` resolviendo `source`/`target` (TEXT) → `node_idx` (uint32) via hashmap.

## Arquitectura

```
cpp/functions/viz/
├── graph_sources.h                  # NEW: GraphLoadFn typedef + decls
├── graph_sources.cpp                # NEW: graph_load_from_operations + stream
├── graph_sources.md                 # NEW
└── (futuras impls JSON/JSONL/GraphML iran aqui mismo)

cpp/tests/
├── test_graph_sources.cpp           # NEW
└── fixtures/
    └── operations_test.db           # NEW: small fixture con 10 entities + 15 relations
```

### API

```cpp
// graph_sources.h
namespace graph {

struct GraphLoadStats {
    int  nodes_loaded;
    int  edges_loaded;
    int  types_discovered;
    int  rel_types_discovered;
    int  errors;
    char error_msg[256];
};

typedef bool (*GraphLoadFn)(const char* uri, GraphData* out, GraphLoadStats* stats);

// Caller is owner of out->nodes/edges/types/rel_types after the call (must call graph_free).
bool graph_load_from_operations(const char* db_path, GraphData* out, GraphLoadStats* stats);

void graph_free(GraphData* graph);

// Streaming: poll-based reader for new entities/relations.
// Caller pre-allocates GraphData with capacity > expected max. Stream appends in place.
struct GraphStreamSource;
GraphStreamSource* graph_stream_operations_open(const char* db_path, int poll_ms);
int                graph_stream_pull(GraphStreamSource*, GraphData* graph);  // returns # appended
void               graph_stream_close(GraphStreamSource*);

} // namespace graph
```

### Color por hash de tipo (default)

```cpp
uint32_t default_color_for(const char* type_name) {
    uint32_t h = fnv1a(type_name);
    // Sample from a balanced palette of 16 indigo-friendly colors.
    static const uint32_t palette[16] = { /* RGBA8 */ };
    return palette[h & 0xF];
}
```

## Tareas

### Fase 1 — `graph_load_from_operations`

- [ ] **1.1** Implementar funcion: abrir SQLite, query types, query entities, query relations, build hashmap `id→node_idx`, llenar `GraphData`.
- [ ] **1.2** Color default por hash sobre `type` name. Shape default `SHAPE_CIRCLE`. Icon default `0`.
- [ ] **1.3** String pool: vector<string> en el `GraphData` (extender struct con `string_pool` o pasarlo via callback). Decision: campo `char** label_pool; int label_pool_count;` interno + helper `graph_label(graph, idx)`.
- [ ] **1.4** Manejo de errores: si la BD no existe / no tiene tabla `entities`, retornar `false` con `error_msg` poblado.
- [ ] **1.5** `graph_free` libera todo lo que `_load_*` alocó. Importante: el caller no deberia tener que diferenciar quien libero — la API es uniforme.

### Fase 2 — Streaming

- [ ] **2.1** `graph_stream_operations_open` guarda `MAX(updated_at)` actual de entities y relations.
- [ ] **2.2** `graph_stream_pull`: query `WHERE updated_at > last_seen`, append a `GraphData` (verifica capacity), actualiza last_seen, retorna conteo.
- [ ] **2.3** Pinear nodos nuevos cerca del centroide del padre (si la nueva entity tiene una relacion con una existente) — opcional pero util. Marcar `NF_PINNED` por N frames; otro mecanismo (`flags |= NF_PINNED_TEMP`?) para auto-release. Mantener simple para v1: pinned manual via app.

### Fase 3 — Tests

- [ ] **3.1** Crear fixture `operations_test.db` con un script SQL: 3 entity types (Person/Email/Domain), 2 relation types (owns/connects), 10 entities, 15 relations.
- [ ] **3.2** Test: cargar fixture, verificar conteos, verificar que types_discovered == 3, que `user_data` es deterministico, que las aristas resuelven a indices validos.
- [ ] **3.3** Test stream: insertar nuevas filas en el fixture, hacer pull, verificar append.

### Fase 4 — Frontmatter `.md`

- [ ] **4.1** `graph_sources.md`:
  - `purity: impure` (toca disco)
  - `error_type: error_go_core` (... no aplica en C++ — usar `bool + error_msg` y documentar)
  - `uses_types: [graph_data_cpp_viz, entity_type_cpp_viz, relation_type_cpp_viz]`
  - `tested: true`
  - `params` y `output` semanticos rellenados

### Fase 5 — Cleanup

- [ ] `fn index`.
- [ ] Commit `feat(viz): graph_sources con lector operations.db + streaming`.

## Criterio de done

- [ ] `graph_load_from_operations("apps/registry_dashboard/operations.db", &g, &s)` carga sin errores y devuelve types descubiertos.
- [ ] Tests verdes con fixture.
- [ ] Streaming detecta filas nuevas.
- [ ] La firma `GraphLoadFn` esta definida y documentada — anadir un backend nuevo es una funcion mas con la misma firma.

## Riesgos

| Riesgo | Mitigacion |
|---|---|
| Schemas de operations.db cambian entre apps | Tomar solo `id`, `type`, `source`, `target`, `weight` — campos estables. Resto via `metadata` JSON opcional |
| Relations con source/target a entities inexistentes | Skip + incrementar `stats.errors` |
| Crecimiento de string pool | Aceptable; un `entity.id` medio es ~32 bytes, 100k = 3 MB |
| Stream perdiendo updates si timestamps son iguales | Usar `(updated_at, id)` como tuple para tiebreak; o anadir un `seq` autoincrement si fuera necesario |
