feat(0049k): graph_explorer wiring + close issue 0049

- cpp/CMakeLists.txt: register projects/osint_graph/apps/graph_explorer/
  via add_subdirectory pattern (igual que registry_dashboard).
- dev/feature_flags.json: osint_graph_v1 = true (enabled_at 2026-04-30).
- dev/issues/{0049,0049k} → dev/issues/completed/. README index actualizado.

La app vive en su sub-repo dataforge/graph_explorer (push hecho al cerrar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 00:14:31 +02:00
parent f9be7aad58
commit decc468531
5 changed files with 11 additions and 4 deletions
@@ -0,0 +1,168 @@
# 0049 — OSINT graph viewer + GPU graph rendering system
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049 |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature — proyecto C++ multi-fase |
## Dependencias
| ID | Título | Estado | Requerido |
|----|--------|--------|-----------|
| 0042 | C++ layout_storage publico | completado | si |
| 0043 | C++ apps standardize shell | completado | si |
| 0048 | C++ visual tests CI gate | completado | si |
**Bloqueada por:** ninguna (todas las deps cerradas).
**Desbloquea:** apps tipo Maltego local + cualquier app del registry que necesite visualizacion de grafos a escala.
---
## Objetivo
Construir un **sistema de visualizacion de grafos GPU-accelerated, agnostico del backend de datos**, y sobre el una app `graph_explorer` para OSINT/ontologia. El renderer debe escalar a 50k+ nodos sin caida de fps, soportar layouts (force, grid, circular, radial, jerarquico, fixed), formas/iconos/colores per-tipo, edges direccionales y con estilos, drag-and-drop, multiseleccion, labels con politica, y filtros por tipo. Datos cargables desde `operations.db` de cualquier app del registry como primer source, con abstraccion funcional preparada para JSON/JSONL/GraphML.
## Contexto
**Lo que existe hoy** (`cpp/functions/viz/`):
- `graph_renderer` — instanced quad rendering en OpenGL 3.3 con SDF circular. CPU rebuilds vertex buffers cada frame.
- `graph_force_layout` — Barnes-Hut quadtree en CPU, una iteracion por frame.
- `graph_viewport` — wrapper ImGui con pan/zoom/hit-testing.
- `graph_types``GraphNode`/`GraphEdge` minimos.
**Lo que limita escalar:**
1. Force layout en CPU domina a partir de ~5k nodos.
2. Renderer aloca y resube buffers cada frame (`glBufferData`).
3. Aristas reconstruidas vertex-by-vertex en CPU.
4. Sin shapes, iconos, edge styles, flechas, filtros, labels.
5. Bound a OpenGL 3.3 (sin compute shaders ni SSBOs).
**Caso de uso final:**
Visor tipo Maltego local con extraccion de entidades por tipos sobre `analysis/ontology_graph` (OSINT banca espanola, ya en marcha). Recoleccion masiva → entities/relations llegan en streaming → visualizacion en tiempo real.
## Arquitectura
```
projects/osint_graph/ # NEW project
├── project.md # NEW
├── apps/
│ └── graph_explorer/ # NEW app (sub-repo dataforge/graph_explorer)
│ ├── app.md
│ ├── CMakeLists.txt
│ ├── main.cpp
│ ├── data.{h,cpp} # dispatcher de GraphLoadFn
│ ├── views.{h,cpp} # toolbar/legend/inspector/stats
│ └── types_registry.{h,cpp} # carga types.yaml para visual override
├── analysis/ # vacio inicial
└── vaults/
└── osint_data -> ~/vaults/osint_graph/ # symlink
cpp/functions/viz/ # extensiones + funciones nuevas
├── graph_types.{h,cpp} # MOD — modelo extendido (type_id, flags, icons...)
├── graph_renderer.{h,cpp} # MOD — RGBA8, orphan, TBO, shapes, iconos, flechas
├── graph_force_layout.{h,cpp} # MOD — auto-pause
├── graph_force_layout_gpu.{h,cpp} # NEW — compute shader + spatial hash
├── graph_layouts.{h,cpp} # NEW — radial, hierarchical, fixed (consolida)
├── graph_viewport.{h,cpp} # MOD — lasso, multi-select, drag-pin, callbacks
├── graph_labels.{h,cpp} # NEW — render con LabelPolicy
├── graph_icons.{h,cpp} # NEW — atlas Tabler en textura
└── graph_sources.{h,cpp} # NEW — graph_load_from_operations + stream
cpp/framework/app_base.cpp # MOD — bump GL 3.3 → 4.3 core
.claude/rules/cpp_apps.md # YA AÑADIDO en sesion previa
```
### Abstraccion funcional `GraphSource`
Misma firma para todos los backends — swap por puntero a funcion, sin virtuals:
```cpp
typedef bool (*GraphLoadFn)(const char* uri, GraphData* out, GraphLoadStats* stats);
bool graph_load_from_operations(const char*, GraphData*, GraphLoadStats*);
bool graph_load_from_json (const char*, GraphData*, GraphLoadStats*); // futuro
bool graph_load_from_jsonl (const char*, GraphData*, GraphLoadStats*); // futuro
bool graph_load_from_graphml (const char*, GraphData*, GraphLoadStats*); // futuro
struct GraphStreamSource;
GraphStreamSource* graph_stream_operations_open(const char* db_path, int poll_ms);
int graph_stream_pull(GraphStreamSource*, GraphData*);
void graph_stream_close(GraphStreamSource*);
```
## Desglose multi-issue
Este issue se implementa en 11 sub-issues independientes. Cada sub-issue es autocontenido — debe compilar, pasar tests, no romper master. Trunk-based development obligatorio (ver `.claude/rules/apps_tbd.md`).
| Sub-issue | Rama | Alcance | Estado |
|-----------|------|---------|--------|
| [0049a](0049a-osint-graph-setup.md) | issue/0049a-osint-graph-setup | Crear proyecto `osint_graph` + vault + sub-repo Gitea de la app | pendiente |
| [0049b](0049b-cpp-bump-gl-43.md) | issue/0049b-cpp-bump-gl-43 | Bump OpenGL 3.3 → 4.3 core en `app_base` + verificar apps existentes | pendiente |
| [0049c](0049c-graph-renderer-tier1.md) | issue/0049c-graph-renderer-tier1 | RGBA8, orphan buffers, frustum cull aristas, auto-pause force | pendiente |
| [0049d](0049d-graph-edges-vertex-pulling.md) | issue/0049d-graph-edges-vertex-pulling | TBO + vertex pulling para aristas | pendiente |
| [0049e](0049e-graph-types-extended.md) | issue/0049e-graph-types-extended | Modelo extendido: type_id, flags, EntityType/RelationType | pendiente |
| [0049f](0049f-graph-renderer-symbols.md) | issue/0049f-graph-renderer-symbols | Shapes SDF, icon atlas, flechas, edge styles | pendiente |
| [0049g](0049g-graph-source-operations.md) | issue/0049g-graph-source-operations | `graph_load_from_operations` + stream variant | pendiente |
| [0049h](0049h-graph-force-layout-gpu.md) | issue/0049h-graph-force-layout-gpu | Compute shader + spatial hash GPU | pendiente |
| [0049i](0049i-graph-layouts-static.md) | issue/0049i-graph-layouts-static | radial, hierarchical, fixed + viewport extendido | pendiente |
| [0049j](0049j-graph-labels.md) | issue/0049j-graph-labels | `graph_labels` con LabelPolicy via ImDrawList | pendiente |
| [0049k](0049k-graph-explorer-app.md) | issue/0049k-graph-explorer-app | App `graph_explorer` + indexado + push final | pendiente |
### Feature flag
Nombre: `osint_graph_v1`
Se activa al cerrar `0049k` cuando `graph_explorer` compila, consume operations.db y muestra grafos OSINT con todas las features (shapes, iconos, layouts, labels, filtros).
### Progreso por fase
- [ ] **0049a** — proyecto osint_graph + vault + sub-repo
- [ ] **0049b** — bump GL 4.3
- [ ] **0049c** — renderer Tier 1 (perf)
- [ ] **0049d** — edges via vertex pulling (perf)
- [ ] **0049e** — modelo de datos extendido (breaking change a graph_types)
- [ ] **0049f** — shapes/iconos/edge-styles/flechas (renderer extendido)
- [ ] **0049g** — source operations.db + streaming
- [ ] **0049h** — force layout GPU compute
- [ ] **0049i** — layouts estaticos + viewport extendido
- [ ] **0049j** — labels con politica
- [ ] **0049k** — app graph_explorer + flag activado
## Decisiones de diseno
1. **OpenGL 4.3 core** (no 3.3): habilita compute shaders + SSBOs, simplifica el layout GPU drasticamente. Trade: GPUs ~2012+ obligatorias (todas las modernas — aceptable).
2. **Spatial hash grid GPU** en vez de Barnes-Hut: Barnes-Hut quadtree es muy duro en GPU sin punteros. Spatial hash es uniforme en carga y suficiente visualmente para el caso OSINT.
3. **Modelo de datos extendido es breaking change** controlado: la unica consumidora actual es `demos_graph` en `primitives_gallery`; se migra en el mismo sub-issue (0049e).
4. **GraphSource es funcional, no OO**: punteros a funcion con misma firma, swap trivial. Sin virtuals, sin templates, sin std::function en hot path.
5. **types.yaml externo** define visual mapping (color/shape/icon per entity type). El renderer no sabe nada de OSINT — solo lee tablas.
6. **CPU mirror de posiciones** se mantiene siempre (8 bytes × N) para hit-test, drag, labels — leido via `glGetBufferSubData` 1x/frame. Trivial coste, simplifica todo.
7. **Cada app C++ es sub-repo Gitea** (`apps_tbd.md` + `cpp_apps.md`): `graph_explorer` se crea con `.git` apuntando a `dataforge/graph_explorer`.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| Bump GL 4.3 rompe apps existentes en HW antiguo | Validar en 0049b sobre las 4 apps actuales en Linux + Windows cross-compile antes de mergear |
| Compute shader complexity (0049h) | Caer back a CPU layout es trivial — la API es identica |
| Streaming desde operations.db sin estabilidad de positions | Pin nodos nuevos cerca del padre por N frames, luego release |
| Demo `demos_graph` queda roto durante 0049e | Migrar `demos_graph.cpp` en el mismo sub-issue con tests visuales |
| Atlas de iconos Tabler ocupa espacio | 512×512 RGBA = 1 MB en VRAM, despreciable |
## Criterio de done global
- [ ] `graph_explorer` abre cualquier `apps/<x>/operations.db` del registry y lo visualiza con tipos descubiertos automaticamente.
- [ ] 50k nodos + 200k aristas a 60fps en GPU integrada con layout corriendo.
- [ ] Drag, multi-select (Ctrl+click + Shift+lasso), filter-by-type, fit-view operativos.
- [ ] Labels visibles para selected/hovered + top-N por tamaño con LabelPolicy configurable.
- [ ] Iconos Tabler renderizados dentro de cada nodo segun `EntityType.icon_id`.
- [ ] Edges direccionales con flechas + estilos solid/dashed/dotted segun `RelationType.style`.
- [ ] `types.yaml` opcional que mergea defaults con override visual por tipo.
- [ ] `fn index` completa y todas las funciones nuevas aparecen con `uses_functions` correcto.
- [ ] Tests Catch2 verdes para `graph_layouts`, `graph_icons`, `graph_force_layout_gpu`, `graph_sources`.
- [ ] Visual test golden actualizado para `demos_graph` con el nuevo modelo.
- [ ] Sub-repo `dataforge/graph_explorer` pushed con master limpio.
- [ ] Feature flag `osint_graph_v1` = `true`.
@@ -0,0 +1,231 @@
# 0049k — App `graph_explorer` (proyecto `osint_graph`)
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049k |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature — parte de [#0049](0049-osint-graph-viewer.md) — **integracion final + activacion del feature flag** |
## Dependencias
**Bloqueada por:**
- [0049a](0049a-osint-graph-setup.md) — proyecto + sub-repo
- [0049f](0049f-graph-renderer-symbols.md) — renderer completo
- [0049g](0049g-graph-source-operations.md) — source operations.db
- [0049h](0049h-graph-force-layout-gpu.md) — layout GPU
- [0049i](0049i-graph-layouts-static.md) — layouts estaticos + viewport
- [0049j](0049j-graph-labels.md) — labels
**Desbloquea:** activacion del feature flag `osint_graph_v1`.
---
## Objetivo
Construir la app C++ `graph_explorer` en `projects/osint_graph/apps/graph_explorer/`, agnostica del backend, capaz de abrir cualquier `operations.db` del registry y visualizarlo con shapes/iconos/layouts/filtros/labels. Cumplir la regla `cpp_apps.md` y `apps_tbd.md`.
## Contexto
Toda la libreria de visualizacion ya existe en `cpp/functions/viz/` tras 0049bj. Esta app es el consumer final que orquesta:
- `graph_load_from_operations` para cargar datos.
- `types_registry` (modulo local) para mergear `types.yaml` opcional con tipos descubiertos.
- `graph_force_layout_gpu` (o CPU) corriendo segun toggle.
- `graph_renderer` + `graph_viewport` + `graph_labels` para visualizacion.
- `fn_ui::*` (toolbar, modal, select, etc.) para chrome.
## Arquitectura
```
projects/osint_graph/apps/graph_explorer/
├── .git/ # sub-repo dataforge/graph_explorer (creado en 0049a)
├── app.md # NEW
├── CMakeLists.txt # NEW
├── main.cpp # NEW
├── data.{h,cpp} # NEW: dispatcher GraphLoadFn
├── views.{h,cpp} # NEW: Toolbar, Legend, Inspector, Stats
├── types_registry.{h,cpp} # NEW: lee types.yaml y mergea
└── examples/
└── types.yaml # NEW: ejemplo OSINT con Person/Email/Domain/...
```
### `app.md` frontmatter
```yaml
---
name: graph_explorer
lang: cpp
domain: viz
description: "Visor de grafos GPU-accelerated agnostico del backend. Lee operations.db de cualquier app del registry y permite explorar entidades/relaciones con shapes/iconos/layouts/filtros."
tags: [imgui, graph, osint, visualization, gpu]
uses_functions:
- graph_renderer_cpp_viz
- graph_force_layout_cpp_viz
- graph_force_layout_gpu_cpp_viz
- graph_layouts_cpp_viz
- graph_viewport_cpp_viz
- graph_labels_cpp_viz
- graph_icons_cpp_viz
- graph_sources_cpp_viz
- toolbar_cpp_core
- modal_dialog_cpp_core
- select_cpp_core
- text_input_cpp_core
- tree_view_cpp_core
- page_header_cpp_core
- fullscreen_window_cpp_core
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "projects/osint_graph/apps/graph_explorer"
repo_url: "https://gitea-.../dataforge/graph_explorer"
---
```
### CLI
```bash
graph_explorer [--input operations <path>] [--types <yaml>] [--layout force|grid|...]
graph_explorer apps/registry_dashboard/operations.db
graph_explorer --types projects/osint_graph/apps/graph_explorer/examples/types.yaml \
apps/element_agents/operations.db
```
### Layout de ventanas
```
┌──────────────────────────────────────────────────────────┐
│ MainMenuBar (View | Settings | About) │
├─[Toolbar: Open | Layout: [force▼] | Filters | Fit | …]──┤
├─Legend──┬───────────────────────────────────────┬─Inspector─┤
│ Type │ │ id: ... │
│ ☑ Person│ │ type: ... │
│ ☑ Email │ Viewport (FBO) │ metadata… │
│ ☐ Domain│ │ │
│ Rels: │ │ Neighbors:│
│ ☑ owns │ │ • node A │
│ ... │ │ • node B │
├─────────┴───────────────────────────────────────┴────────────┤
│ Stats: nodes=12345 edges=54321 fps=60 energy=0.04 sel=2 │
└──────────────────────────────────────────────────────────┘
```
## Tareas
### Fase 1 — Esqueleto
- [ ] **1.1** Crear `app.md` con frontmatter completo.
- [ ] **1.2** Crear `CMakeLists.txt` siguiendo `cpp_apps.md`. Listar todas las funciones del registry usadas explicitamente.
- [ ] **1.3** `main.cpp` minimo con `fn::run_app(cfg, render)` + parseo de `--input`/`--types`/`--layout`.
- [ ] **1.4** Registrar la app en `cpp/CMakeLists.txt` con el patron de `registry_dashboard`:
```cmake
set(_GE_DIR ${CMAKE_SOURCE_DIR}/../projects/osint_graph/apps/graph_explorer)
if(EXISTS ${_GE_DIR}/CMakeLists.txt)
add_subdirectory(${_GE_DIR} ${CMAKE_BINARY_DIR}/apps/graph_explorer)
endif()
```
### Fase 2 — `data.{h,cpp}` (dispatcher de sources)
- [ ] **2.1** Implementar dispatcher segun `--input`:
```cpp
bool load_graph(const InputArgs& args, GraphData* out, GraphLoadStats* stats) {
if (args.kind == INPUT_OPERATIONS) return graph_load_from_operations(args.uri, out, stats);
// futuro: json/jsonl/graphml
stats->errors++; return false;
}
```
- [ ] **2.2** Helper para reload (re-llamar la misma `GraphLoadFn` con la misma URI).
### Fase 3 — `types_registry.{h,cpp}`
- [ ] **3.1** Parser de `types.yaml`:
```yaml
entities:
- name: Person
color: "#5B8DEF"
shape: circle
icon: ti-user
- name: Email
color: "#58CA8C"
shape: square
icon: ti-mail
relations:
- name: owns
color: "#888888"
style: solid
```
- [ ] **3.2** Helper `apply_types_yaml(GraphData&, const ParsedTypes&)`: para cada `EntityType`/`RelationType` del grafo cuyo `name` matchee en el yaml, sobrescribir `color`/`shape`/`icon_id`/`style`. Tipos no encontrados se quedan con default (color por hash).
- [ ] **3.3** Iconos: mapear nombre `ti-user` → codepoint Tabler usando una tabla en el header `icons_tabler.h` (helpers ya existentes o anadir uno nuevo `tabler_codepoint_by_name(const char*)`).
- [ ] **3.4** Construccion del `IconAtlas`: collect codepoints distintos del yaml + fallback (icono "?" para no encontrados), llamar `graph_icons_build(...)`.
### Fase 4 — `views.{h,cpp}` (paneles)
- [ ] **4.1** **Toolbar**: `Open file…`, `Layout selector` (`select`), `Filters…` (modal con checkboxes por tipo), `Fit view`, `Save layout`, `Export PNG`.
- [ ] **4.2** **Legend**: lista de tipos con color swatch, icono y toggle de visibilidad. Toggle aplica a todos los nodos del tipo: `for each node where type_id == t: flags ^= NF_VISIBLE`. Igual para relaciones.
- [ ] **4.3** **Inspector**: cuando hay seleccion de un solo nodo, mostrar `id`, `type`, `metadata` (raw JSON formateado), lista de vecinos directos clickables (click cambia seleccion).
- [ ] **4.4** **Stats**: linea fija con counts + fps + energia.
- [ ] **4.5** Paneles toggleables via `AppConfig::panels`.
### Fase 5 — Persistencia
- [ ] **5.1** `graph_explorer.db` SQLite junto al exe con tabla `layouts(graph_hash TEXT, node_id TEXT, x REAL, y REAL, pinned INT, updated_at)`.
- [ ] **5.2** `graph_hash` = hash del path del input (operations.db) para diferenciar entre grafos.
- [ ] **5.3** `Save layout` boton en la toolbar: snapshot de posiciones + flags.
- [ ] **5.4** Al cargar un grafo conocido (mismo hash), pre-aplicar las posiciones guardadas.
- [ ] **5.5** Layout de paneles ImGui via `layout_storage` (ya existe como funcion publica desde 0042).
### Fase 6 — `types.yaml` ejemplo
- [ ] **6.1** Crear `examples/types.yaml` con ~10 tipos OSINT comunes (Person, Email, Domain, Phone, Org, IBAN, Account, Document, Address, Url) y 5 relaciones (owns, knows, located_in, transfers_to, member_of).
### Fase 7 — TBD trabajo
- [ ] **7.1** Trabajar en rama `quick/graph-explorer` o `issue/0049k-graph-explorer-app` segun preferencia.
- [ ] **7.2** Commits atomicos por panel/feature.
- [ ] **7.3** Merge `--no-ff` a master del sub-repo `dataforge/graph_explorer`.
- [ ] **7.4** Push del sub-repo + push de fn_registry con la nueva ubicacion + `app.md`.
### Fase 8 — Indexado + flag
- [ ] **8.1** `./fn index` desde la raiz.
- [ ] **8.2** Verificar:
```sql
SELECT id, name, project_id FROM apps WHERE id='graph_explorer_cpp_viz';
```
- [ ] **8.3** Activar feature flag en `dev/feature_flags.json`:
```json
"osint_graph_v1": { "enabled": true, "issue": "0049", ... }
```
- [ ] **8.4** Mover el issue principal 0049 + todos los sub-issues 0049ak a `dev/issues/completed/` y actualizar README.
### Fase 9 — Verificacion end-to-end
- [ ] **9.1** Abrir `apps/registry_dashboard/operations.db` y verificar que se ven entidades.
- [ ] **9.2** Abrir un dataset OSINT real (cuando exista) o un fixture en `~/vaults/osint_graph/raw/`.
- [ ] **9.3** Verificar todas las features: filtrado por tipo, drag, multi-select, lasso, layouts, labels, save/load layout.
## Criterio de done
- [ ] `graph_explorer apps/registry_dashboard/operations.db` arranca y muestra el grafo de entidades.
- [ ] Con `--types examples/types.yaml`, los tipos se ven con shapes/iconos/colores correctos.
- [ ] 50k nodos cargan y se navegan a 60fps con layout GPU.
- [ ] Layouts intercambiables en runtime via toolbar.
- [ ] Filtros, drag, multi-select, lasso, labels funcionando.
- [ ] Persistencia de layout entre sesiones.
- [ ] App registrada en registry.db con uses_functions correcto.
- [ ] Sub-repo `dataforge/graph_explorer` con master limpio y pushed.
- [ ] Feature flag `osint_graph_v1` = `true`.
- [ ] Issue 0049 + sub-issues movidos a completed.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| Parser YAML pesa: anadir libreria | Usar yaml-cpp si ya esta vendoreada; si no, parser minimal hand-rolled (10 tipos no necesita full YAML) |
| Mapeo `ti-user` → codepoint requiere tabla nueva | Generar a partir del `icons_tabler.h` existente con un script una sola vez |
| Inspector con metadata grande satura el panel | Truncar a ~512 chars + scroll del panel |
| Save layout en grafo grande es lento | UPSERT por nodo es O(N) con prepared statement; para 50k = ~100 ms aceptable en boton, no en frame |
| Operations.db de un app esta locked si la app corre | SQLite con `mode=ro` o `immutable=1` en el URI |