fix(infra): gradle_run detecta android-sdk — issue 0076 #2

Open
dataforge wants to merge 538 commits from auto/0076-gradle-sdk-detect into master
15 changed files with 1742 additions and 0 deletions
Showing only changes of commit 7c09255c8a - Show all commits
+1
View File
@@ -22,3 +22,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
+148
View File
@@ -0,0 +1,148 @@
## Estandarizacion de apps C++ del registry
**Fuentes autoritativas:**
- `cpp/PATTERNS.md` — checklist y esqueleto del app shell (`fn::run_app`, AppConfig, panels, layouts, Settings, About).
- `cpp/DESIGN_SYSTEM.md` — identidad visual (`fn_tokens`, ThemeMode, equivalencias `@fn_library` ↔ C++).
Esta regla NO duplica esos documentos — los señala como obligatorios y añade convenciones estructurales que no aparecen alli.
### 1. Ubicacion
| Caso | Donde vive |
|---|---|
| App independiente | `cpp/apps/<nombre>/` |
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
### 2. Estructura minima
```
<app_dir>/
CMakeLists.txt # usa add_imgui_app(target ...)
app.md # frontmatter de registro (ver §4)
main.cpp # entry: parseo de args + fn::run_app + render()
[data.{h,cpp}] # opcional: capa de datos (DB / HTTP / archivos)
[views.{h,cpp}] # opcional: composicion de paneles
[<modulo>.{h,cpp}] # opcional: dominio especifico
[vendor/] # opcional: deps no comunes (se prefieren las globales en cpp/vendor/)
[.git/] # cada app es su propio repo Gitea (ver §6)
```
**Reglas de split:**
- `main.cpp` SIEMPRE — punto de entrada con `int main()` + `fn::run_app(...)` + funcion `render()`.
- Si la app supera ~400 lineas en `main.cpp`, partir en `data.{h,cpp}` (carga/persistencia) + `views.{h,cpp}` (UI por panel).
- Modulos especificos del dominio en archivos propios (`compiler.cpp` en `shaders_lab`, `data_http.cpp` en `registry_dashboard`).
- NO crear archivos de "utilidades genericas" dentro de la app — eso va al registry como funcion (`cpp/functions/...`).
### 3. CMakeLists.txt
Patron canonico:
```cmake
add_imgui_app(<target>
main.cpp
[extra_modules.cpp]
# Funciones del registry usadas (paths absolutos):
${CMAKE_SOURCE_DIR}/functions/<dominio>/<funcion>.cpp
...
)
target_include_directories(<target> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(<target> PRIVATE [SQLite::SQLite3] [imgui_node_editor] ...)
if(WIN32)
set_target_properties(<target> PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
```
Reglas:
- Usar SIEMPRE la macro `add_imgui_app(target ...)` — gestiona enlace con `fn_framework` y copia de TTFs.
- Listar explicitamente cada `.cpp` del registry usado (no glob). Hace visible el grafo de dependencias.
- NO listar `tokens.cpp`, `icon_font.cpp`, `app_settings.cpp`, `app_about.cpp`, `fps_overlay.cpp`, `panel_menu.cpp`, `app_menubar.cpp`, `layouts_menu.cpp`, `gl_loader.cpp`, `layout_storage.cpp` — viven en `fn_framework` y dan multiple-definition si se duplican.
- En `WIN32`, marcar `WIN32_EXECUTABLE TRUE` para apps GUI (sin consola).
### 4. app.md (frontmatter)
Plantilla minima para apps C++:
```yaml
---
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
description: "Frase corta — lo que hace y por que existe."
tags: [imgui, ...] # si es service, anadir 'service'
uses_functions: # IDs del registry — el indexer NO deduce C++
- <nombre>_cpp_<dominio>
- ...
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/<name>" o "projects/<proyecto>/apps/<name>"
repo_url: "https://gitea-.../dataforge/<name>"
---
```
Reglas:
- `uses_functions` se rellena a mano con los IDs de las funciones del registry usadas en `CMakeLists.txt`. Auditar con: `sqlite3 registry.db "SELECT id FROM apps WHERE id='<id>';"` + revisar diffs.
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
- `repo_url` apunta al sub-repo en Gitea (ver §6).
### 5. Registro en `cpp/CMakeLists.txt`
Cada app nueva se registra al final de `cpp/CMakeLists.txt`:
```cmake
# --- <app_name> ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/<name>/CMakeLists.txt)
add_subdirectory(apps/<name>)
endif()
```
Para apps en proyectos (fuera del arbol `cpp/`):
```cmake
# --- <app_name> (lives in projects/<proj>/apps/) ---
set(_<NAME>_DIR ${CMAKE_SOURCE_DIR}/../projects/<proj>/apps/<name>)
if(EXISTS ${_<NAME>_DIR}/CMakeLists.txt)
add_subdirectory(${_<NAME>_DIR} ${CMAKE_BINARY_DIR}/apps/<name>)
endif()
```
El `if(EXISTS ...)` hace el registro tolerante a apps no clonadas (cada app es sub-repo separado).
### 6. Sub-repo Gitea (TBD obligatorio)
Cada app C++ es su propio repo en `dataforge/<name>` con branch `master`. Esto significa:
- El directorio `<app_dir>/` esta en el `.gitignore` de `fn_registry` (excepto `app.md`).
- El propio directorio tiene `.git/` apuntando al sub-repo.
- TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/<NNNN>-<slug>` o `quick/<slug>`, mergear a `master` con `--no-ff`.
- Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`.
### 7. Convenciones de runtime
Cumplir el checklist completo de `cpp/PATTERNS.md`. Resumen de lo que NUNCA debe aparecer en una app:
| Anti-patron | Sustituir por |
|---|---|
| `glfwInit()` en `main` | `fn::run_app(cfg, render)` |
| `ImGui::StyleColorsDark()` | `cfg.theme = ThemeMode::FnDark` (default) |
| `ImVec4(0.5,0.5,0.5,1)` | `fn_tokens::colors::*` |
| `ImGui::Begin(u8"\xEF...")` | `ImGui::Begin(TI_HOME " ...")` |
| Menubar inline cada frame | `cfg.panels` + `cfg.layouts_cb` |
| About hardcoded en un panel | `cfg.about = {...}` |
| `gl*` directo sin loader | `cfg.init_gl_loader = true` |
| Tabla SQLite en la raiz del repo | `<app_dir>/<app>.db` (operations.db es solo para entities/relations/executions) |
### 8. Tests visuales (recomendado, no obligatorio)
Si la app tiene componentes que se quieren proteger contra regresiones visuales, anadir un demo en `cpp/apps/primitives_gallery/demos_<dominio>.cpp` que use los mismos componentes/funciones del registry. El sistema de capture-and-compare de `primitives_gallery --capture` funciona como golden-image gate (ver final de `cpp/PATTERNS.md`).
### 9. Decisiones que cada app debe tomar y documentar en su `app.md`
- `viewports`: `true` (default) si las ventanas pueden arrastrarse fuera del main; `false` si la app necesita estar siempre embebida.
- `init_gl_loader`: `true` si llama `gl*` directo (renderers GPU custom como `graph_renderer`); `false` si solo usa ImGui/ImPlot.
- `about` info: nombre, version (semver), descripcion 1 frase.
- Persistencia: `<app>.db` SQLite junto al exe; nunca tocar `registry.db` ni `operations.db` salvo lectura.
- Modo CLI: si la app acepta args, documentarlos en el `app.md` con ejemplos.
+6
View File
@@ -3,5 +3,11 @@
"enabled": true,
"issue": "0007",
"description": "Sistema propio de orquestacion de DAGs para reemplazar Dagu. Incluye parser YAML, executor con paralelismo, process manager, execution store SQLite, scheduler cron, CLI y web frontend."
},
"osint_graph_v1": {
"enabled": false,
"issue": "0049",
"description": "Visor de grafos GPU-accelerated agnostico del backend (graph_explorer en projects/osint_graph) + sistema de renderer extendido (shapes, iconos Tabler, edge styles, flechas, layouts force-GPU/radial/hierarchical, labels con politica). Activado al cerrar 0049k.",
"added": "2026-04-29"
}
}
+168
View File
@@ -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`.
+78
View File
@@ -0,0 +1,78 @@
# 0049b — Bump OpenGL 3.3 → 4.3 core en `cpp/framework`
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049b |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | infraestructura — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** ninguna (ortogonal a 0049a).
**Desbloquea:** [0049h](0049h-graph-force-layout-gpu.md) (compute shaders necesitan 4.3) y simplifica [0049f](0049f-graph-renderer-symbols.md).
---
## Objetivo
Subir el contexto OpenGL del framework de 3.3 core a 4.3 core para habilitar compute shaders, SSBOs, e image load/store. Verificar que las 4 apps C++ existentes siguen funcionando.
## Contexto
Hoy `cpp/framework/app_base.cpp` pide GL 3.3 core (`glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); MINOR, 3`). Limita el renderer GPU del grafo: sin compute, sin SSBO, sin atomic counters. Subir a 4.3 es un cambio de una sola linea en el shell pero requiere validar que ningun shader del registry usa caracteristicas removidas (no las hay — 4.3 core es superset compatible) y que ningun driver target falla.
## Arquitectura
```
cpp/framework/
└── app_base.cpp # MOD: GL_CONTEXT_VERSION_MAJOR/MINOR → 4/3
```
Posible adaptacion en `cpp/functions/gfx/gl_loader.cpp` si los punteros 4.3 no estan cargados (revisar).
## Tareas
### Fase 1 — Cambio
- [ ] **1.1** En `app_base.cpp`, cambiar:
```cpp
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
```
- [ ] **1.2** En `gl_loader.cpp`, anadir punteros 4.x usados por sub-issues posteriores (compute, SSBO, atomic counter). Solo declarar los que se vayan a usar — no inundar.
### Fase 2 — Verificacion en apps existentes
- [ ] **2.1** Linux native build: `cmake --build cpp/build/linux --target shaders_lab registry_dashboard primitives_gallery chart_demo -j$(nproc)`. Cero warnings nuevos.
- [ ] **2.2** Run de cada app y captura visual basica (shaders_lab abre canvas, dashboard carga datos, primitives_gallery navega demos, chart_demo renderiza).
- [ ] **2.3** Windows cross-compile: `cmake --build cpp/build/windows ...`. Mismas apps, mismo check.
### Fase 3 — Tests visuales
- [ ] **3.1** Regenerar goldens: `cpp/scripts/update_goldens.sh`.
- [ ] **3.2** `cpp/scripts/run_tests.sh` debe terminar verde — incluye `test_visual` con tolerancia 1%.
- [ ] **3.3** Revisar diffs en goldens: si cambia algo > tolerancia, investigar (deberia ser idempotente al subir version).
### Fase 4 — Cleanup
- [ ] Commit en master: `feat(framework): bump OpenGL 3.3 → 4.3 core context`.
- [ ] Verificar que `apps_tbd.md` se respeta (rama corta, merge --no-ff a master).
## Criterio de done
- [ ] Las 4 apps existentes compilan en Linux y Windows.
- [ ] `cpp/scripts/run_tests.sh` verde.
- [ ] `glGetString(GL_VERSION)` reporta 4.3+ en runtime.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| HW de pruebas no soporta 4.3 | Toda GPU ~2012+ lo soporta; WSL Mesa software soporta 4.3+ |
| Cambio de profile rompe alguna deprecation | 4.3 core ya excluye fixed-function igual que 3.3 core; no hay regresion esperable |
| Visual goldens cambian por driver | Aceptar regenerar; si diff es semantico, investigar shader |
+105
View File
@@ -0,0 +1,105 @@
# 0049c — `graph_renderer` Tier 1: RGBA8, orphan buffers, frustum cull, auto-pause
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049c |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | mejora rendimiento — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049b](0049b-cpp-bump-gl-43.md) (recomendado pero no estricto — cambios funcionan en 3.3 tambien).
**Desbloquea:** [0049d](0049d-graph-edges-vertex-pulling.md), demos perf-realistas para issues posteriores.
---
## Objetivo
Optimizaciones baratas y de gran impacto sobre `graph_renderer.cpp` y `graph_force_layout` para subir de ~5k nodos a ~20k nodos a 60fps en GPU integrada **sin cambiar la API publica**.
## Contexto
Hoy el renderer:
- Empaqueta colores como 4 floats × N (16 bytes/nodo) en el instance buffer.
- Llama `glBufferData` cada frame → driver realloca el VBO.
- Sube todas las aristas siempre, aunque esten fuera del viewport.
- Force layout corre cada frame aunque la energia sea minima (estado convergido).
## Arquitectura
```
cpp/functions/viz/
├── graph_renderer.{h,cpp} # MOD
├── graph_renderer.md # MOD: bump version (1.x)
├── graph_force_layout.{h,cpp} # MOD: helper auto_pause
└── graph_force_layout.md # MOD
```
Sin cambios en la API publica — son optimizaciones internas.
## Tareas
### Fase 1 — Color packing RGBA8
- [ ] **1.1** En el instance buffer, cambiar layout de `(x, y, size, r, g, b, a)` floats a `(x, y, size, color_rgba8)` donde `color_rgba8` es uint32 packed.
- [ ] **1.2** Ajustar shader vertex de nodos: `layout(location=3) in uint a_color; vec4 col = unpackUnorm4x8(a_color);`.
- [ ] **1.3** Ajustar el packing en CPU: helper `pack_rgba8(r,g,b,a) = (a<<24)|(b<<16)|(g<<8)|r`.
- [ ] **1.4** Idem para el buffer de aristas (color por vertex → uint32 por vertex).
### Fase 2 — Orphan buffer pattern
- [ ] **2.1** Reemplazar `glBufferData(GL_ARRAY_BUFFER, sz, data, GL_DYNAMIC_DRAW)` por:
```cpp
glBufferData(GL_ARRAY_BUFFER, capacity_bytes, nullptr, GL_STREAM_DRAW); // orphan
glBufferSubData(GL_ARRAY_BUFFER, 0, used_bytes, data);
```
- [ ] **2.2** Mantener `capacity_bytes` interno en el `GraphRenderer` y crecer al doble si `used_bytes > capacity`.
### Fase 3 — Frustum cull aristas
- [ ] **3.1** Calcular AABB visible en world coords:
```cpp
float wx0 = cam_x - (width/2)/zoom; float wx1 = cam_x + (width/2)/zoom;
float wy0 = cam_y - (height/2)/zoom; float wy1 = cam_y + (height/2)/zoom;
```
- [ ] **3.2** En el bucle de aristas, skip si AABB de la arista (segmento source→target con margen) no intersecta el viewport AABB.
- [ ] **3.3** Nodos: similar — skip nodos cuyo AABB (centro ± size) cae fuera. Como son draws instanced, el cull se hace empaquetando solo los visibles en el instance buffer (mantener un counter `visible_count`).
### Fase 4 — Auto-pause force layout
- [ ] **4.1** En `graph_force_layout.h`, anadir helper:
```cpp
// Devuelve true si la energia ha caido bajo el umbral durante N frames consecutivos.
bool graph_force_layout_should_pause(float energy, float threshold, int min_consecutive);
```
- [ ] **4.2** Documentar uso en el `.md`. El consumer guarda un contador interno; el helper es puro.
- [ ] **4.3** Migrar `demos_graph.cpp` para usarlo y para no invocar `_step` cuando `paused == true`. Boton "Resume" ya existe.
### Fase 5 — Tests + benchmark
- [ ] **5.1** Test Catch2 sobre `pack_rgba8`/`unpack_rgba8`: roundtrip exacto.
- [ ] **5.2** Test Catch2 sobre `graph_force_layout_should_pause`: secuencias artificiales.
- [ ] **5.3** Benchmark manual en `demos_graph` con N=20000: anotar fps antes/despues en el .md de la funcion (`notes:`).
### Fase 6 — Cleanup
- [ ] Bump version del .md de `graph_renderer` a 1.1.0 y de `graph_force_layout` a 1.1.0.
- [ ] `fn index` y verificar.
- [ ] Commit `perf(viz): graph_renderer Tier 1 (RGBA8, orphan, cull) + force_layout auto-pause`.
## Criterio de done
- [ ] `demos_graph` con 20k nodos a 60fps en GPU integrada de pruebas.
- [ ] Tests verdes.
- [ ] `nvidia-smi` o `radeontop` muestran que la CPU baja respecto al baseline (perfilar con Tracy si TRACY_ENABLE).
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| `unpackUnorm4x8` no esta en GL 3.3 sin extension | Esta en core 4.0+; con bump 0049b ya disponible. Si 0049b no se mergea antes, fallback a `(color>>0)&0xff)/255.0` manual |
| Frustum cull provoca pop-in en bordes | Anadir margen del 10% del viewport AABB |
| Crecimiento de capacity buffer en streaming | Crecer al doble; documentar capacity inicial razonable (4096 nodos) |
@@ -0,0 +1,104 @@
# 0049d — Aristas via vertex pulling con TBO
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049d |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | mejora rendimiento — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049c](0049c-graph-renderer-tier1.md) (orphan + RGBA8 ya en sitio).
---
## Objetivo
Eliminar la reconstruccion del buffer de aristas en CPU cada frame. Las posiciones de nodos viven en un Texture Buffer Object (TBO); el vertex shader de aristas hace fetch de source/target con `gl_VertexID`. El buffer de aristas es estatico (`source_idx`, `target_idx`, `type_id`), solo cambian las posiciones — y eso ya estaba para los nodos.
## Contexto
Tras 0049c, el bottleneck principal restante es el upload de 12 floats × E aristas cada frame. Para 100k aristas: 4.8 MB/frame. Vertex pulling lo elimina por completo.
## Arquitectura
```
cpp/functions/viz/
├── graph_renderer.{h,cpp} # MOD: anadir TBO + buffer estatico de aristas
└── graph_renderer.md # MOD: bump 1.1 → 1.2
```
Cambios:
1. `GraphRenderer` gana `unsigned int node_pos_tbo, node_pos_tex` (texture buffer y su sampler view).
2. `GraphRenderer` gana `unsigned int edge_static_vbo` con `(uint source, uint target, uint color, uint flags)` por arista — subido una vez (o cuando cambia el grafo, no cada frame).
3. Buffer de posiciones de nodos (`vec2[]`) se sube como TBO vinculado al `node_pos_tex`.
4. Vertex shader de aristas:
```glsl
#version 430 core
layout(location=0) in uint a_source;
layout(location=1) in uint a_target;
layout(location=2) in uint a_color;
layout(location=3) in uint a_flags;
uniform samplerBuffer u_node_pos; // vec2[] como TBO
out vec4 v_color;
void main() {
int idx = (gl_VertexID & 1) == 0 ? int(a_source) : int(a_target);
vec2 p = texelFetch(u_node_pos, idx).xy;
// mismo MVP que ya estaba
gl_Position = u_mvp * vec4(p, 0.0, 1.0);
v_color = unpackUnorm4x8(a_color);
}
```
Cada arista renderiza con `glDrawArrays(GL_LINES, 0, edge_count*2)`. El fragment shader ya existia.
## Tareas
### Fase 1 — TBO de posiciones
- [ ] **1.1** En `graph_renderer_create`: crear `node_pos_buf` (VBO) + `node_pos_tex` (texture buffer view) con `glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, node_pos_buf)`.
- [ ] **1.2** En `graph_renderer_draw`: empaquetar posiciones de nodos en un buffer flotante `(x,y) × N` y `glBufferSubData` al `node_pos_buf` (orphan + sub).
- [ ] **1.3** Antes del draw de aristas: `glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_BUFFER, node_pos_tex); glUniform1i(u_node_pos_loc, 0);`.
### Fase 2 — Buffer estatico de aristas
- [ ] **2.1** Anadir API interna `mark_edges_dirty()` que regenera el `edge_static_vbo`.
- [ ] **2.2** Si el caller pasa `graph.edges_revision != cached_revision`, regenerar. Para el primer paso, usar siempre `cached==false → regenerar` y optimizar luego con un campo `revision` en `GraphData`.
- [ ] **2.3** Layout: `struct EdgeStatic { uint source; uint target; uint color_rgba8; uint flags; }` (16 bytes por arista).
### Fase 3 — Shaders de aristas
- [ ] **3.1** Reescribir vertex shader como en la arquitectura (4.3 core).
- [ ] **3.2** Verificar fragment shader no necesita cambios.
- [ ] **3.3** Reverificar `glLineWidth`/edge_alpha siguen funcionando.
### Fase 4 — Bench + tests
- [ ] **4.1** `demos_graph` con 20k nodos + 100k aristas a 60fps en GPU integrada.
- [ ] **4.2** Profile con Tracy: el bucle de aristas en CPU debe desaparecer.
- [ ] **4.3** Test Catch2 minimo: render a FBO + readback, verificar que un grafo conocido produce un frame no-vacio (smoke test, no golden).
### Fase 5 — Cleanup
- [ ] Bump version `graph_renderer` 1.1.0 → 1.2.0.
- [ ] `fn index`.
- [ ] Commit `perf(viz): graph_renderer edges via TBO + vertex pulling`.
## Criterio de done
- [ ] CPU ms del frame para 100k aristas baja a < 0.5 ms (medible con Tracy o reloj manual).
- [ ] Render visualmente identico al pre-cambio.
- [ ] Demos de la galeria afectados (`demos_graph`) sin regresiones.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| `samplerBuffer` no funciona en alguna driver Linux | GL 4.3 core lo exige; si falla en WSL software, marcar test como SKIP igual que `test_visual` |
| Mantener `edges_revision` complica la API | Empezar con regenerar siempre y optimizar despues — no premature optimization |
| 4 bytes desperdiciados por arista (`flags`) | Justificado por alineacion + futuras flechas/styles |
+187
View File
@@ -0,0 +1,187 @@
# 0049e — Modelo de datos extendido: `GraphNode`/`GraphEdge` + `EntityType`/`RelationType`
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049e |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | breaking change controlado — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049d](0049d-graph-edges-vertex-pulling.md) (cambios pegan en mismo `graph_types.h`; mejor encadenarlos).
**Desbloquea:** [0049f](0049f-graph-renderer-symbols.md), [0049g](0049g-graph-source-operations.md), [0049i](0049i-graph-layouts-static.md).
---
## Objetivo
Extender `graph_types.h` con el modelo agnostico necesario para shapes/iconos/filtros/labels/streaming, sin acoplar a ningun backend de datos. Migrar `demos_graph.cpp` (unico consumer actual) en el mismo sub-issue.
## Contexto
El modelo actual es minimo (`x, y, vx, vy, size, color, community`). Para soportar:
- Shapes y iconos per-tipo
- Filtrado por tipo y per-nodo
- Selection / hover / pin
- Mapeo de vuelta a la entidad real del backend
- Aristas con tipo, direccion, estilo
Se necesita `type_id`, `flags`, `*_override`, `label_idx`, `user_data` en nodos; `type_id`, `flags`, `style_override` en aristas; tablas `EntityType[]` / `RelationType[]`.
## Arquitectura
```
cpp/functions/viz/
├── graph_types.h # MOD: structs extendidos + enums
├── graph_types.cpp # MOD: helpers (graph_node, graph_edge actualizados)
├── graph_types.md # MOD: bump
├── graph_renderer.{h,cpp} # MOD: leer type_id, flags, overrides al pintar
├── graph_force_layout.{h,cpp} # MOD: respetar flags.pinned
├── graph_viewport.{h,cpp} # MOD: setear flags.selected/hovered
cpp/apps/primitives_gallery/
└── demos_graph.cpp # MOD: migrar al modelo nuevo
cpp/tests/
└── test_graph_types.cpp # NEW
```
### Modelo final
```cpp
// graph_types.h
namespace graph {
enum NodeFlags : uint8_t {
NF_NONE = 0,
NF_PINNED = 1 << 0,
NF_VISIBLE = 1 << 1,
NF_SELECTED = 1 << 2,
NF_HOVERED = 1 << 3,
};
enum EdgeFlags : uint8_t {
EF_NONE = 0,
EF_DIRECTED = 1 << 0,
EF_VISIBLE = 1 << 1,
};
enum Shape : uint8_t {
SHAPE_CIRCLE = 0, SHAPE_SQUARE, SHAPE_DIAMOND, SHAPE_HEX,
SHAPE_TRIANGLE, SHAPE_ROUNDED_SQUARE,
};
enum EdgeStyle : uint8_t {
EDGE_SOLID = 0, EDGE_DASHED, EDGE_DOTTED,
};
struct GraphNode {
float x, y, vx, vy;
uint16_t type_id;
uint8_t shape_override; // 0 = use type, otherwise SHAPE_*
uint8_t flags; // NF_* mask, default NF_VISIBLE
uint32_t color_override; // 0 = use type, !=0 = RGBA8
float size_override; // 0 = use type
uint32_t label_idx; // index into consumer's string pool
uint64_t user_data; // opaque, app uses to map back to its DB
};
struct GraphEdge {
uint32_t source, target;
uint16_t type_id;
uint8_t style_override; // 0 = use type, otherwise EDGE_*
uint8_t flags; // EF_* mask
float weight;
uint32_t label_idx;
};
struct EntityType {
uint32_t color; // RGBA8
uint8_t shape; // SHAPE_*
uint16_t icon_id; // 0 = no icon
float default_size;
const char* name; // for UI/debug, not for render
};
struct RelationType {
uint32_t color;
uint8_t style; // EDGE_*
float width;
const char* name;
};
struct GraphData {
GraphNode* nodes; int node_count; int node_capacity;
GraphEdge* edges; int edge_count; int edge_capacity;
EntityType* types; int type_count;
RelationType* rel_types; int rel_type_count;
// bounds, etc. (existentes)
};
} // namespace graph
```
## Tareas
### Fase 1 — Tipos
- [ ] **1.1** Reescribir `graph_types.h` con el modelo de arriba.
- [ ] **1.2** Helpers `graph_node(...)` / `graph_edge(...)` para construccion ergonomica con defaults.
- [ ] **1.3** Crear `types/viz/graph_node.md`, `graph_edge.md`, `entity_type.md`, `relation_type.md` con frontmatter completo (algebraic: product). Si ya existen, bump version.
### Fase 2 — Renderer
- [ ] **2.1** En `graph_renderer.cpp`, resolver visual final del nodo:
```cpp
uint32_t color = n.color_override ? n.color_override : graph.types[n.type_id].color;
uint8_t shape = n.shape_override ? n.shape_override : graph.types[n.type_id].shape;
float size = n.size_override > 0 ? n.size_override : graph.types[n.type_id].default_size;
if (!(n.flags & NF_VISIBLE)) skip;
```
- [ ] **2.2** Idem para aristas: estilo y color del `RelationType`. Skip si `!(EF_VISIBLE)` o si los dos endpoints no son visibles.
- [ ] **2.3** Mientras tanto, todavia NO renderizamos shapes (eso es 0049f) — todos como circulo. Pero el dispatch ya esta cableado.
### Fase 3 — Force layout
- [ ] **3.1** En `graph_force_layout.cpp`, sustituir `n.pinned` por `(n.flags & NF_PINNED)`.
### Fase 4 — Viewport
- [ ] **4.1** En `graph_viewport.cpp`, setear `flags |= NF_HOVERED` y `flags |= NF_SELECTED` en lugar de los campos actuales.
- [ ] **4.2** Limpiar el flag al cambiar el target (clear-then-set).
### Fase 5 — Migrar `demos_graph`
- [ ] **5.1** Crear un `EntityType` y un `RelationType` por defecto (paleta antigua → 8 entity types, 1 relation type).
- [ ] **5.2** Asignar `type_id` por cluster como antes.
- [ ] **5.3** Setear `flags = NF_VISIBLE` en cada nodo creado y `EF_VISIBLE` en cada arista.
- [ ] **5.4** Ejecutar y verificar visual identico al pre-cambio.
### Fase 6 — Tests
- [ ] **6.1** `cpp/tests/test_graph_types.cpp`: helpers, defaults, flag manipulation, lookup color/shape con/sin override.
- [ ] **6.2** Visual golden de `demos_graph` regenerado si pixel-diff > tolerancia (no deberia).
### Fase 7 — Cleanup
- [ ] Bump version `graph_types`, `graph_renderer`, `graph_force_layout`, `graph_viewport`.
- [ ] `fn index`.
- [ ] Commit `feat(viz): graph_types modelo extendido + EntityType/RelationType + flags`.
## Criterio de done
- [ ] `demos_graph` visualmente identico al pre-cambio.
- [ ] Tests Catch2 verdes.
- [ ] Visual golden ok.
- [ ] `fn show graph_node_cpp_viz` (y los otros tipos) reflejan el nuevo schema.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| Tamaño del struct sube | `GraphNode` ~40 bytes y `GraphEdge` ~24 bytes — aceptable para 100k entidades (~4 MB) |
| Migrar `demos_graph` rompe golden | Regenerar golden si la diff es solo cosmetica; investigar si es semantica |
| Otros consumers ocultos | Solo `demos_graph` consume hoy — verificado con grep antes de mergear |
+151
View File
@@ -0,0 +1,151 @@
# 0049f — Renderer extendido: shapes SDF, icon atlas, flechas, edge styles
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049f |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049e](0049e-graph-types-extended.md) (necesita `shape`, `icon_id`, `flags.directed`, `style`).
---
## Objetivo
Extender el fragment shader del renderer con una libreria de SDF (circulo, cuadrado, diamante, hex, triangulo, rounded square), un atlas de iconos Tabler renderizado a textura, flechas direccionales en aristas, y estilos solid/dashed/dotted. Una sola draw call para todos los nodos, una para todas las aristas.
## Contexto
Maltego/OSINT requiere distinguir entidades de un vistazo: Person ≠ Email ≠ Domain. Color solo no escala — necesitamos forma + icono. El stack ya tiene Tabler como font (`cpp/functions/core/icons_tabler.h`); aqui lo bakeamos a una textura para que cada nodo pueda llevar un icono dentro.
## Arquitectura
```
cpp/functions/viz/
├── graph_renderer.{h,cpp} # MOD: shaders extendidos + uniform sampler de iconos
├── graph_renderer.md # MOD: bump version
├── graph_icons.{h,cpp} # NEW: builder del atlas
├── graph_icons.md # NEW
└── (fragment shader incluye sdf_*.glsl inline)
cpp/tests/
└── test_graph_icons.cpp # NEW
```
### API `graph_icons`
```cpp
struct IconAtlas;
struct IconRegion {
uint16_t id; // = posicion en el array al construir
float u0, v0, u1, v1; // UVs en la textura
};
// Construye una textura 512x512 RGBA con `count` iconos Tabler renderizados a 32px.
// Codepoints son los valores `TI_*` del header.
IconAtlas* graph_icons_build(const uint16_t* codepoints, int count, int icon_px = 32);
unsigned int graph_icons_texture(const IconAtlas*); // GL texture id
const IconRegion* graph_icons_region(const IconAtlas*, uint16_t icon_id);
void graph_icons_destroy(IconAtlas*);
```
### Shaders extendidos
Vertex shader de nodos ya pasa `type_id`, `shape`, `icon_id` por instance. Fragment shader compone:
```glsl
float sdf_circle (vec2 uv) { return length(uv - 0.5) - 0.5; }
float sdf_square (vec2 uv) { vec2 d = abs(uv - 0.5) - 0.5; return max(d.x, d.y); }
float sdf_diamond(vec2 uv) { vec2 d = abs(uv - 0.5); return d.x + d.y - 0.5; }
float sdf_hex (vec2 uv) { ... }
float sdf_triangle(vec2 uv){ ... }
float sdf_rrect (vec2 uv) { vec2 d = abs(uv - 0.5) - 0.5 + r; return length(max(d,0.0)) - r; }
float pick_sdf(uint shape, vec2 uv) {
switch (shape) {
case 0u: return sdf_circle(uv);
case 1u: return sdf_square(uv);
case 2u: return sdf_diamond(uv);
...
}
}
void main() {
float d = pick_sdf(v_shape, v_uv);
float aa = fwidth(d);
float a = 1.0 - smoothstep(0.0, aa, d);
if (a < 0.001) discard;
vec3 col = v_color.rgb;
if (v_icon_id != 0u) {
// UV del icono dentro del atlas: (uv - 0.5) * scale + region_center
vec2 atlas_uv = mix(vec2(v_icon_u0, v_icon_v0), vec2(v_icon_u1, v_icon_v1), v_uv);
vec4 ic = texture(u_icon_atlas, atlas_uv);
col = mix(col, vec3(1.0), ic.a * 0.85);
}
frag_color = vec4(col, a * v_color.a);
}
```
### Aristas direccionales con flecha
Cada arista pasa de 2 vertices (line) a 4 vertices: 2 para el segmento + 2 para el triangulo de la flecha (solo si `flags & EF_DIRECTED`). Indices 0-1 = linea, 2-3 = triangulo apuntando al target. Vertex shader calcula la flecha en world coords usando direccion target-source y tamaño constante en pixels.
### Edge styles
Fragment shader de aristas recibe `arc_length` (interpolado linealmente entre source y target en pixels). Para `style=DASHED`: `if (mod(arc_length, 8.0) > 4.0) discard;`. Para `DOTTED`: similar con periodo y duty diferentes.
## Tareas
### Fase 1 — `graph_icons`
- [ ] **1.1** Crear `graph_icons.{h,cpp,md}`. Implementar `_build` usando `stb_truetype` (o ImGui font baker) para rasterizar codepoints Tabler a una bitmap 512×512.
- [ ] **1.2** Layout simple: grid 16×16 a 32px por celda → 256 iconos por atlas.
- [ ] **1.3** Subir como GL texture RGBA8 con linear filtering.
- [ ] **1.4** Tests: build de 10 iconos conocidos; verificar que la textura tiene contenido en las regiones esperadas.
### Fase 2 — Shaders SDF
- [ ] **2.1** Implementar las 6 funciones SDF en GLSL.
- [ ] **2.2** `pick_sdf` con switch por `shape_id`.
- [ ] **2.3** Pasar `shape`, `icon_id`, `icon_u0/v0/u1/v1` por instance. Layout actualizado.
- [ ] **2.4** Compose icon overlay en fragment.
### Fase 3 — Aristas direccionales + estilos
- [ ] **3.1** Cambiar `glDrawArrays(GL_LINES, ...)` por geometry expansion en CPU/shader: 4 vertices por arista, los 2 ultimos solo se usan si `EF_DIRECTED`.
- [ ] **3.2** Vertex shader calcula posicion de la flecha (10 px constante en pantalla).
- [ ] **3.3** Fragment shader recibe `arc_length` y descarta segun `style`.
### Fase 4 — Demo
- [ ] **4.1** Crear `cpp/apps/primitives_gallery/demos_graph_styles.cpp`: grafo pequeño (~30 nodos) con 6 EntityTypes (uno por shape), 3 RelationTypes (solid/dashed/dotted), aristas direccionales mezcladas. Iconos Tabler representativos: `TI_USER`, `TI_MAIL`, `TI_GLOBE`, `TI_PHONE`, `TI_BUILDING`, `TI_DATABASE`.
- [ ] **4.2** Anadirlo a `demos.h` y al menu de la galeria.
- [ ] **4.3** Visual golden generado.
### Fase 5 — Cleanup
- [ ] Bump version `graph_renderer` 1.2.0 → 1.3.0; `graph_icons` 1.0.0.
- [ ] `fn index`.
- [ ] Commit `feat(viz): renderer shapes/iconos/flechas/edge-styles`.
## Criterio de done
- [ ] `demos_graph_styles` muestra todas las shapes + iconos + flechas + estilos visualmente correctos.
- [ ] Sigue siendo 1 draw call por nodos y 1 por aristas.
- [ ] Test golden estable.
- [ ] Tests `test_graph_icons` verdes.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| `switch` en GLSL ramifica → menos eficiente | Acceptable a estas escalas; se puede unfold luego con `#define` por tipo si hace falta |
| Atlas baker mete artifacts en bordes | Padding 2px entre celdas |
| Flechas ocupan area visible del nodo target | Acortar el segmento de linea por el radio del nodo target en vertex shader |
| Codepoints Tabler con caracteres compuestos | Usar solo los basicos del header `icons_tabler.h` (ya validados) |
+145
View File
@@ -0,0 +1,145 @@
# 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 |
+136
View File
@@ -0,0 +1,136 @@
# 0049h — `graph_force_layout_gpu`: compute shader + spatial hash
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049h |
| **Estado** | pendiente |
| **Prioridad** | media-alta |
| **Tipo** | feature performance — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049b](0049b-cpp-bump-gl-43.md) (compute shaders), [0049e](0049e-graph-types-extended.md) (`flags.pinned`).
---
## Objetivo
Implementar el force-directed layout en GPU usando compute shaders y SSBOs. Repulsion via spatial hash grid (no Barnes-Hut), atraccion por aristas, integracion con clamp y respeto de `NF_PINNED`. API consistente con la version CPU para que el consumer pueda swappear.
## Contexto
A 50k+ nodos el CPU layout (Barnes-Hut, ~5-10 ms/frame con 20k nodos) impide 60fps con margen. GPU compute con grid hash:
- Cada celda contiene punteros a sus nodos (atomic insert).
- Repulsion: cada nodo lee solo su celda + 8 vecinas (3×3) → O(N · density) en vez de O(N log N).
- Sin estructuras dinamicas → todo arrays planos en SSBO.
## Arquitectura
```
cpp/functions/viz/
├── graph_force_layout_gpu.h # NEW
├── graph_force_layout_gpu.cpp # NEW
├── graph_force_layout_gpu.md # NEW
cpp/functions/viz/shaders/ # NEW dir (o inline strings)
├── force_clear_grid.comp # NEW: zero counters
├── force_build_grid.comp # NEW: insert nodes en celdas (atomic counters)
├── force_repulsion.comp # NEW: leer celdas vecinas, acumular fuerza
├── force_attraction.comp # NEW: por arista, acumular spring force
└── force_integrate.comp # NEW: v += f, clamp, x += v, respeta pinned
cpp/tests/
└── test_graph_force_layout_gpu.cpp # NEW
```
### API
```cpp
struct ForceLayoutGPU;
ForceLayoutGPU* graph_force_layout_gpu_create(int max_nodes, int max_edges,
int grid_cells_per_side = 64);
void graph_force_layout_gpu_upload(ForceLayoutGPU*, const GraphData&); // copy positions/edges to GPU
float graph_force_layout_gpu_step (ForceLayoutGPU*, GraphData&,
const ForceLayoutConfig&); // returns total energy
void graph_force_layout_gpu_readback(ForceLayoutGPU*, GraphData&); // sync GPU positions back to CPU mirror
void graph_force_layout_gpu_destroy(ForceLayoutGPU*);
```
### Buffers GPU
| SSBO | Layout | Tamaño |
|---|---|---|
| `positions` | `vec2[N]` | 8 × N |
| `velocities` | `vec2[N]` | 8 × N |
| `forces` | `vec2[N]` | 8 × N (working) |
| `flags` | `uint[N]` | 4 × N |
| `edges` | `uvec2[E]` | 8 × E |
| `weights` | `float[E]` | 4 × E |
| `grid_counts`| `uint[G²]` | 4 × 64² = 16 KB |
| `grid_cells` | `uint[G²][K]` | 4 ×× max_nodes_per_cell (32) |
Para 100k nodos: ~3 MB en SSBOs — trivial.
## Tareas
### Fase 1 — Esqueleto + buffer alloc
- [ ] **1.1** Crear `ForceLayoutGPU` con `max_nodes`/`max_edges`. Crear todos los SSBOs.
- [ ] **1.2** `_upload`: empaqueta posiciones/aristas/flags y llama `glBufferSubData`.
- [ ] **1.3** Compilar los 5 compute shaders al crear (helper `compile_compute(src)`).
### Fase 2 — Compute shaders
- [ ] **2.1** `force_clear_grid.comp`: 1 thread por celda, zero counter.
- [ ] **2.2** `force_build_grid.comp`: 1 thread por nodo, calcula celda, `atomicAdd(grid_counts[ci], 1)`, escribe en `grid_cells[ci][slot]` si `slot < K`.
- [ ] **2.3** `force_repulsion.comp`: 1 thread por nodo. Recorre las 9 celdas vecinas, para cada otro nodo calcula `F = repulsion / dist²`. Acumula en `forces[i]`.
- [ ] **2.4** `force_attraction.comp`: 1 thread por arista, atomic add a `forces[source]` y `forces[target]`.
- [ ] **2.5** `force_integrate.comp`: 1 thread por nodo. Si `flags & NF_PINNED`, skip. `v = damping*v + F`, clamp a `max_velocity`, `x += v`. Atomic add a `total_energy_ssbo`.
### Fase 3 — Step pipeline
- [ ] **3.1** `_step` despacha los 5 compute shaders en orden con `glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT)` entre cada uno.
- [ ] **3.2** Ultimo barrier: `GL_BUFFER_UPDATE_BARRIER_BIT` para que `_readback` vea valores frescos.
- [ ] **3.3** Bounds calculados en CPU tras readback (cheap).
### Fase 4 — Readback eficiente
- [ ] **4.1** `_readback` usa `glGetBufferSubData` solo de `positions` (8 × N bytes). Para 50k nodos = 400 KB; CPU mirror se actualiza cada frame.
- [ ] **4.2** Documentar tradeoff: si el consumer no necesita mirror cada frame (p.ej. solo la app que dibuja con la GPU), `_readback` es opcional.
### Fase 5 — Tests
- [ ] **5.1** Test smoke: 100 nodos, 200 aristas; correr 100 steps; verificar que la energia decrece.
- [ ] **5.2** Test pinned: pinear 1 nodo en (0,0); tras 100 steps debe seguir en (0,0).
- [ ] **5.3** Test consistency CPU-vs-GPU: para un grafo pequeño (50 nodos), la energia tras N steps debe diferir < 5% entre la version CPU y la GPU (no exacto por floating-point + grid approximation, pero comparable).
- [ ] **5.4** Test SKIP en WSL si no hay context con compute (el harness ya skipea si no hay GL context).
### Fase 6 — Demo
- [ ] **6.1** Anadir toggle "GPU layout" en `demos_graph` que swappea CPU ↔ GPU. Mostrar fps y energia en tiempo real.
- [ ] **6.2** Probar con 50k nodos.
### Fase 7 — Cleanup
- [ ] Frontmatter `.md` con `purity: impure`, `uses_types: [graph_data_cpp_viz, force_layout_config_cpp_viz]`.
- [ ] `fn index`.
- [ ] Commit `feat(viz): graph_force_layout_gpu compute + spatial hash`.
## Criterio de done
- [ ] 50k nodos + 200k aristas a 60fps con layout iterativo corriendo.
- [ ] Tests verdes (smoke + pinned + consistency).
- [ ] CPU usage < 10% durante el layout (medible con Tracy).
- [ ] Toggle CPU/GPU en `demos_graph` operativo.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| `max_nodes_per_cell` overflow | Si una celda satura, los excedentes se ignoran (con warning); ajustar `grid_cells_per_side` mas grande o aumentar `K` |
| Atomic contention en repulsion (escrituras a `forces[i]`) | Cada nodo es escrito por un solo thread (su propio); lecturas si concurrentes pero readonly de `positions` |
| Atomic add sobre `forces` en attraction (multiple aristas tocan el mismo nodo) | Usar `atomicAdd` real sobre uvec2 packed, o serializar con un buffer temp y sumar despues. Alternativa: un compute pass por dimension |
| Diferencias driver entre vendors (Mesa, NV, AMD) | Usar `#version 430 core`, evitar features vendor-specific. Test en al menos un GPU NVIDIA y uno AMD/Mesa |
+146
View File
@@ -0,0 +1,146 @@
# 0049i — `graph_layouts` (radial, hierarchical, fixed) + viewport extendido
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049i |
| **Estado** | pendiente |
| **Prioridad** | media |
| **Tipo** | feature — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049e](0049e-graph-types-extended.md) (necesita `flags`).
---
## Objetivo
Consolidar las estrategias de layout estatico en una sola funcion `graph_layouts` (anadiendo `radial`, `hierarchical`, `fixed`), y extender `graph_viewport` con lasso, multi-select acumulativo, drag de seleccion entera y callbacks de menu contextual / double-click.
## Contexto
Hoy `graph_force_layout.cpp` incluye `graph_layout_circular` y `graph_layout_grid` como helpers. Para OSINT son utiles:
- **Radial**: arbol con un nodo raiz seleccionado y sus vecinos en circulos concentricos por hop.
- **Hierarchical** (Sugiyama-style): niveles por tipo o por dependencia (Person → Email → Domain).
- **Fixed**: no-op, las posiciones las pone el caller.
`graph_viewport` ya soporta pan/zoom/click + hit-test. Falta el resto de UX para Maltego.
## Arquitectura
```
cpp/functions/viz/
├── graph_layouts.{h,cpp} # NEW (mueve circular/grid + nuevos)
├── graph_layouts.md # NEW
├── graph_viewport.{h,cpp} # MOD: lasso, multi-select, callbacks
└── graph_viewport.md # MOD: bump
cpp/tests/
├── test_graph_layouts.cpp # NEW
└── test_graph_viewport.cpp # NEW (smoke)
```
### `graph_layouts` API
```cpp
namespace graph {
// Estaticos. Mutan posiciones, respetan NF_PINNED.
void layout_grid (GraphData&, float spacing);
void layout_circular (GraphData&, float radius);
void layout_random (GraphData&, float spread);
void layout_radial (GraphData&, int root_node, float ring_spacing);
void layout_hierarchical(GraphData&, int direction); // 0=LR, 1=RL, 2=TB, 3=BT
void layout_fixed (GraphData&); // no-op
} // namespace graph
```
`graph_force_layout.cpp` deja de exportar `_circular`/`_grid` (delegan a `graph_layouts`). Mantener wrappers deprecados un sub-issue maximo, eliminar antes del cierre de 0049.
### `graph_viewport` extensiones
```cpp
struct GraphViewportCallbacks {
void (*on_context_menu)(int node_idx, ImVec2 screen_pos, void* user) = nullptr;
void (*on_double_click)(int node_idx, void* user) = nullptr;
void* user = nullptr;
};
struct GraphViewportState {
// ... existente
int selected_node; // legacy: ultimo seleccionado
std::vector<int> selection; // NEW: multi-seleccion
bool lasso_active;
ImVec2 lasso_start, lasso_end;
};
// Igual firma que hoy, mas un parametro opcional de callbacks.
void graph_viewport(const char* id, GraphData&, GraphViewportState&,
ImVec2 size, const GraphViewportCallbacks& cb = {});
```
Comportamiento:
- **Click**: limpia seleccion, anade nodo bajo cursor.
- **Ctrl+Click**: toggle nodo en seleccion.
- **Shift+Drag (sin nodo bajo cursor)**: lasso. Al soltar, anade los nodos dentro del rect a la seleccion.
- **Drag con un nodo seleccionado bajo el cursor**: arrastra todos los seleccionados como pinned (set `NF_PINNED` mientras se arrastra; mantener pinned al soltar).
- **Right-click sobre un nodo**: invoca `on_context_menu(idx, screen_pos, user)` si esta seteado.
- **Double-click sobre un nodo**: invoca `on_double_click(idx, user)`.
- **Esc**: limpia seleccion.
## Tareas
### Fase 1 — `graph_layouts`
- [ ] **1.1** Crear `graph_layouts.{h,cpp,md}`. Mover impl de `circular`/`grid` desde `graph_force_layout.cpp`.
- [ ] **1.2** Implementar `layout_radial`: BFS desde `root_node`, posicionar cada hop k en un circulo de radio `k * ring_spacing`, distribuir uniformemente.
- [ ] **1.3** Implementar `layout_hierarchical`: BFS levels por longest-path desde nodos sin in-edges; dentro de cada nivel ordenar por minimo cruce (greedy heuristico — no optimo, pero bueno para la UX OSINT).
- [ ] **1.4** Implementar `layout_fixed`: no-op (recordar que existe la funcion).
- [ ] **1.5** Todas respetan `NF_PINNED`.
### Fase 2 — Viewport multi-select + lasso
- [ ] **2.1** En `graph_viewport.cpp`, implementar el comportamiento de la tabla anterior.
- [ ] **2.2** Lasso: `ImDrawList::AddRect` para feedback visual + AABB hit-test al soltar.
- [ ] **2.3** Drag de seleccion: pin todos los nodos seleccionados al inicio del drag, aplicar el delta a todos, mantener pinned al soltar.
### Fase 3 — Callbacks
- [ ] **3.1** Anadir `GraphViewportCallbacks` y wirear `on_context_menu` (right-click) + `on_double_click`.
- [ ] **3.2** Documentar en el `.md` que el callback se invoca dentro del frame ImGui — el caller puede abrir un popup.
### Fase 4 — Tests
- [ ] **4.1** `test_graph_layouts`: smoke de cada layout sobre un grafo pequeño; verificar que `NF_PINNED` no se mueve; que `radial` distribuye correctamente.
- [ ] **4.2** `test_graph_viewport`: setup de un grafo, simular hit-test programatico (no test interactivo, solo helpers puros).
### Fase 5 — Demo
- [ ] **5.1** Anadir toggle de layout en `demos_graph` (`force | grid | circular | radial | hierarchical | fixed`).
- [ ] **5.2** Anadir lasso + multi-select visible en el demo (text overlay con count seleccionados).
### Fase 6 — Cleanup
- [ ] Bump versions: `graph_layouts` 1.0.0 (nuevo), `graph_viewport` 1.x → 1.x+1.
- [ ] Documentar `params`/`output` en el `.md` para FTS5 search.
- [ ] `fn index`.
- [ ] Commit `feat(viz): graph_layouts (radial/hierarchical/fixed) + viewport multi-select+lasso`.
## Criterio de done
- [ ] Switch entre layouts en el demo es instantaneo.
- [ ] Lasso visible, multi-seleccion acumulativa funcional.
- [ ] Drag de N nodos seleccionados los mueve juntos como pinned.
- [ ] Right-click invoca callback si esta seteado.
- [ ] Tests verdes.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| Hierarchical layout se ve mal en grafos densamente cruzados | Aceptable — Sugiyama optimo es un campo entero; el heuristico es para visualizacion OSINT, no publicacion |
| Multi-select state en GraphViewportState rompe ABI | Es un cambio interno; `selection` es campo nuevo, ok |
| Drag de seleccion gigante (10k nodos) lagueva | Desactivar fuerzas en pinned ya implica que la GPU no los toca. Drag solo aplica delta — O(N seleccionados) trivial |
+124
View File
@@ -0,0 +1,124 @@
# 0049j — `graph_labels`: render de etiquetas con `LabelPolicy`
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049j |
| **Estado** | pendiente |
| **Prioridad** | media |
| **Tipo** | feature — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049e](0049e-graph-types-extended.md) (necesita `label_idx` + `flags`).
---
## Objetivo
Funcion `graph_labels_draw` que renderiza etiquetas de nodos seleccionados/hover/pinned + top-N por importancia, con politica configurable y culling por viewport. Independiente del renderer GPU — usa `ImDrawList` sobre el FBO.
## Contexto
Maltego/OSINT necesita leer "juan@x.com", IBAN, etc. No se pueden mostrar 20k labels — pero se puede mostrar:
- Siempre los selected/hovered/pinned (suelen ser pocos).
- Top-N por tamaño de nodo o grado (configurable).
- Todos cuando el zoom es alto y el nodo mide > X pixels en pantalla.
Esto se decide cada frame. ImDrawList es eficiente y se compone sobre la imagen del FBO ya pintada.
## Arquitectura
```
cpp/functions/viz/
├── graph_labels.h # NEW
├── graph_labels.cpp # NEW
└── graph_labels.md # NEW
cpp/tests/
└── test_graph_labels.cpp # NEW (smoke + culling logic)
```
### API
```cpp
namespace graph {
struct LabelPolicy {
bool always_for_selected = true;
bool always_for_hovered = true;
bool always_for_pinned = false;
int max_visible = 200; // top-N por size + degree
float min_zoom_for_all = 4.0f; // a este zoom, mostrar todos los visibles del viewport
float min_node_pixel_size = 12.0f; // skip si en pantalla mide menos
float font_size = 13.0f; // pixels
uint32_t color = 0xFFFFFFFF; // ABGR
uint32_t bg_color = 0xC8000000; // semi-transparente
float padding_x = 4.0f;
float padding_y = 2.0f;
};
// Callback que devuelve el texto del label dado un node_idx.
// El consumer maneja su propio string pool / metadata.
typedef const char* (*GetLabelFn)(int node_idx, void* user);
// Llamar tras ImGui::Image(...) del FBO. Usa el ImDrawList del current window.
void graph_labels_draw(const GraphData&, const GraphViewportState&,
const LabelPolicy&, GetLabelFn cb, void* user);
} // namespace graph
```
### Algoritmo (cada frame)
1. Determinar AABB visible en world coords desde camera+zoom.
2. Colectar nodos visibles + nodos con `flags & (NF_SELECTED|NF_HOVERED|NF_PINNED)`.
3. Si `zoom >= min_zoom_for_all`: candidatos = todos los visibles del viewport. Else: top-N por `(size * degree)`.
4. Filtrar: `node_pixel_size = node.size * zoom`; skip si `< min_node_pixel_size` (excepto los `always_*`).
5. Para cada candidato superviviente:
- World → screen.
- `text = cb(idx, user)`.
- `ImDrawList::AddRectFilled(bg)` + `AddText(color)` con padding.
6. Limit hard: nunca dibujar mas de `max_visible + |selected| + |hovered| + |pinned|`.
## Tareas
### Fase 1 — Funcion + helpers
- [ ] **1.1** Crear `graph_labels.{h,cpp,md}`. Implementar `_draw` segun el algoritmo.
- [ ] **1.2** Helper interno `score(node) = size * (degree+1)` calculado tras frustum cull para top-N.
- [ ] **1.3** Cache opcional del `degree` por nodo si el consumer la quiere precalcular y pasarsela (parametro avanzado en LabelPolicy o helper aparte). Para v1, calcular o-fly desde edges en O(E) y guardar en un thread_local vector — no critico.
### Fase 2 — Tests
- [ ] **2.1** Test culling: setup grafo de 100 nodos, viewport pequeño, verificar que el numero de labels devuelto (mock callback que cuenta) respeta max_visible.
- [ ] **2.2** Test always_for_selected: setear NF_SELECTED en uno fuera del viewport, verificar que NO se dibuja (selected pero off-screen — segun politica). Decision: documentar comportamiento (default: no, para no spamear).
- [ ] **2.3** Test min_node_pixel_size: zoom bajo, nodo pequeño, no se dibuja.
### Fase 3 — Integrar en `demos_graph`
- [ ] **3.1** Tras la `ImGui::Image(...)` del viewport, llamar `graph_labels_draw` con un callback que devuelve `"#" + node_idx`.
- [ ] **3.2** Anadir controles en demo para variar `LabelPolicy`: max_visible slider, font_size slider, toggle always_*.
### Fase 4 — Cleanup
- [ ] `params`/`output` documentados en `.md`.
- [ ] `fn index`.
- [ ] Commit `feat(viz): graph_labels con LabelPolicy + ImDrawList`.
## Criterio de done
- [ ] En `demos_graph` con 20k nodos: labels visibles para selected/hovered + top-N a fps estable.
- [ ] Zoom alto muestra todos los visibles, zoom bajo solo los importantes — sin saltos bruscos.
- [ ] Tests verdes.
- [ ] No rompe perf: con `LabelPolicy.max_visible = 0` y todos los `always_*` off, la funcion es practicamente gratis.
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| ImDrawList con miles de AddText degrada fps | `max_visible = 200` por default; cap es duro |
| Texto recortado por el clip rect del child window | Si el FBO esta dentro de un BeginChild/EndChild, usar el draw list correcto (probablemente el del window padre con clip ajustado) |
| Cambios de zoom hacen aparecer/desaparecer labels en avalancha | Hysteresis opcional en `min_zoom_for_all` (umbral on != umbral off). Para v1, simple |
| Costo de calcular `degree` cada frame | Aceptable a 100k aristas (un pase O(E)); cachear si se vuelve hot path |
+231
View File
@@ -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 |
+12
View File
@@ -54,3 +54,15 @@
| [0046](completed/0046-cpp-refactor-raw-imgui.md) | Reemplazar raw ImGui en apps por primitivos del registry | completado | media | refactor | — |
| [0047](completed/0047-cpp-tests-foundation.md) | C++ tests foundation (Catch2 + top-20 primitivos) | completado | alta | feature | 0048 |
| [0048](completed/0048-cpp-visual-tests-ci-gate.md) | Visual tests via primitives_gallery + CI gate tested:true | completado | media | feature | — |
| [0049](0049-osint-graph-viewer.md) | OSINT graph viewer + GPU graph rendering system (multi-issue) | pendiente | alta | feature | — |
| [0049a](0049a-osint-graph-setup.md) | Setup proyecto osint_graph + sub-repo graph_explorer | pendiente | alta | infra | parte de 0049 |
| [0049b](0049b-cpp-bump-gl-43.md) | Bump OpenGL 3.3 → 4.3 core en cpp/framework | pendiente | alta | infra | parte de 0049 |
| [0049c](0049c-graph-renderer-tier1.md) | graph_renderer Tier 1: RGBA8, orphan, frustum cull, auto-pause | pendiente | alta | perf | parte de 0049 |
| [0049d](0049d-graph-edges-vertex-pulling.md) | Aristas via vertex pulling con TBO | pendiente | alta | perf | parte de 0049 |
| [0049e](0049e-graph-types-extended.md) | graph_types modelo extendido + EntityType/RelationType | pendiente | alta | feature | parte de 0049 |
| [0049f](0049f-graph-renderer-symbols.md) | Renderer extendido: shapes SDF, icon atlas, flechas, edge styles | pendiente | alta | feature | parte de 0049 |
| [0049g](0049g-graph-source-operations.md) | graph_sources: lector operations.db + abstraccion funcional | pendiente | alta | feature | parte de 0049 |
| [0049h](0049h-graph-force-layout-gpu.md) | graph_force_layout_gpu: compute shader + spatial hash | pendiente | media-alta | feature | parte de 0049 |
| [0049i](0049i-graph-layouts-static.md) | graph_layouts (radial/hierarchical/fixed) + viewport multi-select | pendiente | media | feature | parte de 0049 |
| [0049j](0049j-graph-labels.md) | graph_labels: render etiquetas con LabelPolicy | pendiente | media | feature | parte de 0049 |
| [0049k](0049k-graph-explorer-app.md) | App graph_explorer (proyecto osint_graph) — integracion final | pendiente | alta | feature | parte de 0049 |