diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md index 1adc6558..2a303f79 100644 --- a/.claude/rules/INDEX.md +++ b/.claude/rules/INDEX.md @@ -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 | diff --git a/.claude/rules/cpp_apps.md b/.claude/rules/cpp_apps.md new file mode 100644 index 00000000..c99f70ed --- /dev/null +++ b/.claude/rules/cpp_apps.md @@ -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//` | +| App de un proyecto | `projects//apps//` | + +NUNCA en `cpp/apps//` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`. + +### 2. Estructura minima + +``` +/ + 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 + [.{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( + main.cpp + [extra_modules.cpp] + # Funciones del registry usadas (paths absolutos): + ${CMAKE_SOURCE_DIR}/functions//.cpp + ... +) +target_include_directories( PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries( PRIVATE [SQLite::SQLite3] [imgui_node_editor] ...) + +if(WIN32) + set_target_properties( 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: +lang: cpp +domain: +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++ + - _cpp_ + - ... +uses_types: [] +framework: "imgui" +entry_point: "main.cpp" +dir_path: "cpp/apps/" o "projects//apps/" +repo_url: "https://gitea-.../dataforge/" +--- +``` + +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='';"` + 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 +# --- --- +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps//CMakeLists.txt) + add_subdirectory(apps/) +endif() +``` + +Para apps en proyectos (fuera del arbol `cpp/`): + +```cmake +# --- (lives in projects//apps/) --- +set(__DIR ${CMAKE_SOURCE_DIR}/../projects//apps/) +if(EXISTS ${__DIR}/CMakeLists.txt) + add_subdirectory(${__DIR} ${CMAKE_BINARY_DIR}/apps/) +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/` con branch `master`. Esto significa: +- El directorio `/` 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/-` o `quick/`, 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 | `/.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_.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: `.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. diff --git a/dev/feature_flags.json b/dev/feature_flags.json index 3b482c2c..ee71375b 100644 --- a/dev/feature_flags.json +++ b/dev/feature_flags.json @@ -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" } } diff --git a/dev/issues/0049-osint-graph-viewer.md b/dev/issues/0049-osint-graph-viewer.md new file mode 100644 index 00000000..f825fb53 --- /dev/null +++ b/dev/issues/0049-osint-graph-viewer.md @@ -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//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`. diff --git a/dev/issues/0049b-cpp-bump-gl-43.md b/dev/issues/0049b-cpp-bump-gl-43.md new file mode 100644 index 00000000..903b9a86 --- /dev/null +++ b/dev/issues/0049b-cpp-bump-gl-43.md @@ -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 | diff --git a/dev/issues/0049c-graph-renderer-tier1.md b/dev/issues/0049c-graph-renderer-tier1.md new file mode 100644 index 00000000..12b757c4 --- /dev/null +++ b/dev/issues/0049c-graph-renderer-tier1.md @@ -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) | diff --git a/dev/issues/0049d-graph-edges-vertex-pulling.md b/dev/issues/0049d-graph-edges-vertex-pulling.md new file mode 100644 index 00000000..fd5244b0 --- /dev/null +++ b/dev/issues/0049d-graph-edges-vertex-pulling.md @@ -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 | diff --git a/dev/issues/0049e-graph-types-extended.md b/dev/issues/0049e-graph-types-extended.md new file mode 100644 index 00000000..7dd482b0 --- /dev/null +++ b/dev/issues/0049e-graph-types-extended.md @@ -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 | diff --git a/dev/issues/0049f-graph-renderer-symbols.md b/dev/issues/0049f-graph-renderer-symbols.md new file mode 100644 index 00000000..9ecc595b --- /dev/null +++ b/dev/issues/0049f-graph-renderer-symbols.md @@ -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) | diff --git a/dev/issues/0049g-graph-source-operations.md b/dev/issues/0049g-graph-source-operations.md new file mode 100644 index 00000000..dd91ef38 --- /dev/null +++ b/dev/issues/0049g-graph-source-operations.md @@ -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 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 | diff --git a/dev/issues/0049h-graph-force-layout-gpu.md b/dev/issues/0049h-graph-force-layout-gpu.md new file mode 100644 index 00000000..137b4899 --- /dev/null +++ b/dev/issues/0049h-graph-force-layout-gpu.md @@ -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 × G² × 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 | diff --git a/dev/issues/0049i-graph-layouts-static.md b/dev/issues/0049i-graph-layouts-static.md new file mode 100644 index 00000000..94d0f215 --- /dev/null +++ b/dev/issues/0049i-graph-layouts-static.md @@ -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 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 | diff --git a/dev/issues/0049j-graph-labels.md b/dev/issues/0049j-graph-labels.md new file mode 100644 index 00000000..68175882 --- /dev/null +++ b/dev/issues/0049j-graph-labels.md @@ -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 | diff --git a/dev/issues/0049k-graph-explorer-app.md b/dev/issues/0049k-graph-explorer-app.md new file mode 100644 index 00000000..532599c7 --- /dev/null +++ b/dev/issues/0049k-graph-explorer-app.md @@ -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 0049b–j. 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 ] [--types ] [--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 0049a–k 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 | diff --git a/dev/issues/README.md b/dev/issues/README.md index a91500de..01c771c4 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -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 |