--- name: graph_sources kind: function lang: cpp domain: viz version: "1.0.0" purity: impure signature: "bool graph_load_from_operations(const char* db_path, GraphData* out, GraphLoadStats* stats)" description: "Lectores de grafos para GraphData con firma uniforme GraphLoadFn. Primera implementacion: operations.db (entities + relations) con variante streaming pull-based" tags: [graph, sources, sqlite, operations, streaming, loader, viz] uses_functions: [] uses_types: ["GraphData_cpp_viz", "EntityType_cpp_viz", "RelationType_cpp_viz"] returns: [] returns_optional: false error_type: "error_go_core" imports: [] tested: true tests: ["test_graph_sources"] test_file_path: "cpp/tests/test_graph_sources.cpp" file_path: "cpp/functions/viz/graph_sources.cpp" framework: "imgui" params: - name: db_path desc: "Ruta a un fichero SQLite operations.db (schema con entities + relations). Solo se abre en modo READONLY" - name: out desc: "GraphData destino. La funcion aloca nodes/edges/types/rel_types y un string pool interno; el caller debe liberar con graph_free()" - name: stats desc: "GraphLoadStats opcional con conteos y un buffer error_msg de 256 bytes; si la carga falla, errors > 0 y error_msg describe el motivo (BD ausente, tabla faltante, columna desconocida)" output: "true si la carga fue correcta. false con stats->errors > 0 y error_msg poblado en error. Tras un retorno true: out->nodes/edges apuntan a memoria interna; out->types/rel_types listan los tipos descubiertos con color por hash del nombre y SHAPE_CIRCLE/EDGE_SOLID por defecto. user_data por nodo es FNV1a64 del entity.id (deterministico). label_idx apunta a metadata.name si existe, else entity.name, else entity.id. Streaming: graph_stream_operations_open captura MAX(updated_at, id) actuales; graph_stream_pull devuelve cuantas filas append y crece capacity en x2 si hace falta" --- # graph_sources Lectores de grafos para `GraphData` (issue 0049g). Disenado como un set de funciones con la misma firma `GraphLoadFn` para que anadir un backend nuevo (JSON, JSONL, GraphML, Neo4j export, etc.) sea declarar otra funcion compatible — el resto del codigo (apps, viewport, force layout, renderer) no cambia. ## API ```cpp typedef bool (*GraphLoadFn)(const char* uri, GraphData* out, GraphLoadStats* stats); bool graph_load_from_operations(const char* db_path, GraphData* out, GraphLoadStats* stats); void graph_free(GraphData* graph); const char* graph_label(const GraphData* graph, uint32_t label_idx); // Streaming pull-based (poll cada N ms desde el caller). GraphStreamSource* graph_stream_operations_open(const char* db_path, int poll_ms); int graph_stream_pull(GraphStreamSource*, GraphData*); void graph_stream_close(GraphStreamSource*); ``` ## Mapeo operations.db → GraphData `operations.db` es la BD de cada app del registry. La funcion detecta el schema via `PRAGMA table_info`: | Concepto | Columna preferida | Fallback | |----------|-------------------|----------| | Tipo de entidad | `entities.type_ref` | `entities.type` | | Source / target de relacion | `relations.from_entity` / `relations.to_entity` | `relations.source` / `relations.target` | | Tipo de relacion | `relations.type` | `relations.name` | | Etiqueta de nodo | `metadata.name` (JSON) | `entities.name` o `entities.id` | | Tiebreak streaming | `(updated_at, id)` | `id` solo | Cada valor distinto de `type_ref` produce un `EntityType` con color tomado de un palette de 16 (FNV1a32 sobre el nombre → `palette[h & 0xF]`), `shape = SHAPE_CIRCLE`, `default_size = 6.0`, `icon_id = 0`. La app consumidora puede sobreescribir esa apariencia via `types.yaml` (issue 0049k). `user_data` por nodo es `FNV1a64(entity.id)` — deterministico entre cargas y util como handle estable para joins con metadata externa. ## Errores La funcion no usa excepciones. En error: - Retorna `false`. - `stats->errors >= 1`. - `stats->error_msg` contiene un texto corto (`"open: ..."`, `"missing table: entities"`, `"entities: missing type_ref/type column"`, ...). Relaciones con `from_entity` / `to_entity` que apunten a entities inexistentes se cuentan en `stats->errors` y se descartan — el grafo resultante es siempre consistente. ## Streaming El streaming es pull-based: el caller decide la cadencia y llama `graph_stream_pull` cuando quiere chequear cambios. La fuente guarda `(MAX(updated_at), MAX(id))` al abrir y avanza el cursor con tiebreak `(updated_at, id)`: ```sql WHERE (updated_at > ?) OR (updated_at = ? AND id > ?) ORDER BY updated_at, id ``` `graph_stream_pull` crece la capacidad de `nodes`/`edges` en x2 si hace falta — el caller no necesita pre-allocar. Tipos nuevos descubiertos durante el stream se anaden al final de `types`. Operaciones inversas (deletes) no se propagan — para reset, el caller debe `graph_free` y recargar. ## Memoria `graph_load_from_operations` aloca toda la memoria que cuelga del `GraphData` devuelto (incluido el string pool de labels y los nombres de tipos via `strdup`). El caller libera todo con `graph_free(graph)`. La asociacion `GraphData* → arena` se mantiene en un mapa estatico interno; `graph_label` y `graph_stream_pull` lo consultan para acceder al pool. ## Ejemplo ```cpp GraphData g{}; graph::GraphLoadStats s{}; if (!graph::graph_load_from_operations("apps/script_navegador/operations.db", &g, &s)) { fprintf(stderr, "load failed: %s\n", s.error_msg); return 1; } printf("nodes=%d edges=%d types=%d rel_types=%d\n", s.nodes_loaded, s.edges_loaded, s.types_discovered, s.rel_types_discovered); // Render con graph_renderer + force layout via graph_force_layout. // ... // Watcher en otro hilo: auto* src = graph::graph_stream_operations_open("apps/script_navegador/operations.db", 500); while (running) { sleep_ms(500); int n = graph::graph_stream_pull(src, &g); if (n > 0) printf("appended %d new rows\n", n); } graph::graph_stream_close(src); graph::graph_free(&g); ``` ## Notas - **v1.0** (2026-04-29, issue 0049g): primera version. Lector sincrono + streaming poll-based. Tests con fixture in-memory: 10 entities + 15 relations, 3 entity types, 2 relation types. Determinismo del `user_data` y resolucion de aristas verificados. - La firma `GraphLoadFn` esta diseniada para futuros backends. Anadir uno (ej. `graph_load_from_jsonl`) consiste en declarar una funcion con la misma firma; nada en el resto del pipeline (renderer, layout, viewport) cambia.