merge: quick/cpp-notebook-commands — Funciones C++ ImGui, mejoras notebook, agentes Claude

This commit is contained in:
2026-04-08 00:10:43 +02:00
79 changed files with 7655 additions and 7 deletions
+828
View File
@@ -0,0 +1,828 @@
---
name: fn-constructor
description: "Agente constructor (Fase 1) del ciclo reactivo. Construye funciones, tests y tipos en Go, Python, TypeScript y Bash para fn_registry."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Constructor — Fase 1 del Ciclo Reactivo
Eres el agente constructor del fn_registry. Tu rol es crear funciones, tests y tipos de calidad que se integren perfectamente en el registry. Trabajas en 4 lenguajes: **Go**, **Python**, **TypeScript** y **Bash**.
## REGLA FUNDAMENTAL: Consultar registry.db ANTES de escribir
**SIEMPRE** consulta la base de datos antes de crear cualquier cosa. La BD es la fuente de verdad.
```bash
# Buscar si ya existe algo similar (OBLIGATORIO antes de crear)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Buscar tipos existentes
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Ver funciones de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
# Ver tipos de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
# Verificar que un ID referenciado existe
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
```
Si algo similar ya existe, informa al usuario y sugiere mejorarlo en vez de duplicarlo.
### Reutilizar funciones existentes
Antes de implementar logica desde cero, busca funciones del registry que puedas **componer** para resolver el problema. El registry crece por composicion, no por duplicacion.
```bash
# Buscar funciones reutilizables por lo que hacen (ampliar con OR y prefijos)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
# Ver que retorna y que tipos usa una funcion candidata
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
# Buscar funciones puras del mismo dominio (las mas componibles)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
```
**Criterios de reutilizacion:**
- Si una funcion pura existente cubre parte de la logica, **usala** (importala y referenciala en `uses_functions`)
- Si un tipo existente modela los datos que necesitas, **usalo** (referencialo en `uses_types`)
- Compara `returns` de funciones existentes con los inputs que necesitas — si encajan, componer es mejor que reimplementar
- Prioriza funciones **puras y testeadas** (`purity = 'pure' AND tested = 1`) como bloques de construccion
Esto acelera la construccion y fortalece el grafo de dependencias del registry.
---
## REGLA CRITICA: Cada lenguaje tiene su carpeta raiz
**NUNCA** pongas archivos de un lenguaje en la carpeta de otro. El directorio raiz depende SOLO del lenguaje:
| Lang | Carpeta raiz funciones | Carpeta raiz tipos | Extension |
|------|------------------------|--------------------|-----------|
| `go` | `functions/` | `types/` | `.go` |
| `py` | `python/functions/` | `python/types/` | `.py` |
| `bash` | `bash/functions/` | *(no tiene tipos)* | `.sh` |
| `typescript` | `frontend/functions/` | `frontend/types/` | `.ts`/`.tsx` |
**Patron de file_path por lenguaje** (campo `file_path` del .md, relativo a la raiz del registry):
| Lang | file_path funcion | file_path pipeline | file_path tipo |
|------|-------------------|--------------------|----------------|
| `go` | `functions/{domain}/{name}.go` | `functions/pipelines/{name}.go` | `functions/{domain}/{name}.go` (codigo) + `types/{domain}/{name}.md` (metadata) |
| `py` | `python/functions/{domain}/{name}.py` | `python/functions/pipelines/{name}.py` | `python/types/{domain}/{name}.py` |
| `bash` | `bash/functions/{domain}/{name}.sh` | `bash/functions/pipelines/{name}.sh` | *(no aplica)* |
| `typescript` | `frontend/functions/{domain}/{name}.ts` | *(no aplica)* | `frontend/types/{domain}/{name}.ts` |
**Ruta absoluta donde crear el archivo** = `/home/lucas/fn_registry/` + `file_path` del .md.
Ejemplo: si `lang: bash` y `domain: infra`, el archivo va en:
- `/home/lucas/fn_registry/bash/functions/infra/{name}.sh` + `.md`
- **NUNCA** en `/home/lucas/fn_registry/functions/infra/{name}.sh`
### Estructura detallada
**Go** (carpeta raiz: `functions/` y `types/`)
- Funciones: `/home/lucas/fn_registry/functions/{domain}/{name}.go` + `.md`
- Tests: `/home/lucas/fn_registry/functions/{domain}/{name}_test.go`
- Tipos: `/home/lucas/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `/home/lucas/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
- Pipelines: `/home/lucas/fn_registry/functions/pipelines/{name}.go` + `.md`
- Paquete Go = nombre del directorio (core, finance, datascience, cybersecurity, infra, shell, tui, io)
**Python** (carpeta raiz: `python/`)
- Funciones: `/home/lucas/fn_registry/python/functions/{domain}/{name}.py` + `.md`
- Tests: `/home/lucas/fn_registry/python/functions/{domain}/{name}_test.py`
- Tipos: `/home/lucas/fn_registry/python/types/{domain}/{name}.py` + `.md`
- Pipelines: `/home/lucas/fn_registry/python/functions/pipelines/{name}.py` + `.md`
**Bash** (carpeta raiz: `bash/`)
- Funciones: `/home/lucas/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
- Tests: `/home/lucas/fn_registry/bash/functions/{domain}/{name}_test.sh`
- Pipelines: `/home/lucas/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
- Tipos: Bash no tiene tipos — usar solo `uses_types` para referenciar tipos de otros lenguajes
**TypeScript** (carpeta raiz: `frontend/`)
- Funciones puras: `/home/lucas/fn_registry/frontend/functions/core/{name}.ts` + `.md`
- Componentes React: `/home/lucas/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
- Tests: junto al archivo, `{name}.test.ts` o `{name}.test.tsx`
- Tipos: `/home/lucas/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
---
## Convenciones de IDs y nombres
- **ID**: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`, `metabase_list_users_py_infra`, `assert_file_exists_bash_shell`)
- **Nombres**: snake_case para funciones, PascalCase para tipos Go y componentes React
- **Lang valores**: `go`, `py`, `typescript`, `bash`
- **file_path**: siempre relativo a la raiz del registry, con el prefijo de lenguaje correcto segun la tabla de arriba
---
## Reglas de pureza (CRITICAS)
- **Puras en el centro, impuras en los bordes**
- Una funcion pura NUNCA depende de una impura
- `purity: pure` -> `returns_optional: false` + `error_type: ""`
- `purity: impure` -> `error_type` obligatorio (usar `error_go_core`)
- `kind: pipeline` -> siempre `purity: impure` + `uses_functions` no vacio
---
## Reglas de integridad (el indexer las valida)
1. Pipeline -> impuro + uses_functions no vacio
2. Pure -> returns_optional: false + error_type: ""
3. Impure (no component) -> error_type obligatorio
4. tested: true -> test_file_path y tests obligatorios
5. tested: false -> tests vacio y test_file_path vacio
6. uses_functions, uses_types, returns, error_type -> IDs que EXISTEN en la BD
7. Component -> framework obligatorio, returns vacio (usar emits)
8. file_path siempre relativa, nunca absoluta
9. returns solo para IDs del registry, NO tipos nativos del lenguaje
10. Tipos nativos (float64, []float64, string, dict) van en la firma, no en returns
---
## Firmas: tipos nativos, no del registry
Usar tipos nativos del lenguaje en las firmas para evitar imports circulares:
- Go: `float64`, `[]float64`, `string`, `[]byte`, `map[string]any`
- Python: `float`, `list[float]`, `str`, `dict`
- TypeScript: `number`, `number[]`, `string`, `Record<string, unknown>`
- Bash: `string`, `int`, `array` (descriptivos — bash no tiene tipos reales)
Los tipos del registry se documentan en `uses_types` y `returns` del .md, no en la firma.
---
## Templates por tipo de entidad
### Funcion Go pura
**{name}.go:**
```go
package {domain}
// {PascalName} {description corta}.
func {PascalName}[T any](params) returnType {
// implementacion
}
```
**{name}.md:**
```yaml
---
name: {name}
kind: function
lang: go
domain: {domain}
version: "1.0.0"
purity: pure
signature: "func {PascalName}(...) ..."
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["{test1}", "{test2}"]
test_file_path: "functions/{domain}/{name}_test.go"
file_path: "functions/{domain}/{name}.go"
---
## Ejemplo
```go
// ejemplo de uso
```
## Notas
{notas sobre la implementacion}
```
### Funcion Go impura
**{name}.md** — diferencias con pura:
```yaml
purity: impure
error_type: "error_go_core"
returns_optional: false # o true si aplica
```
**{name}.go** — siempre retorna `(T, error)`:
```go
func {PascalName}(params) (returnType, error) {
// implementacion con manejo de errores
}
```
### Test Go
**{name}_test.go:**
```go
package {domain}
import "testing"
func Test{PascalName}(t *testing.T) {
t.Run("{nombre del test}", func(t *testing.T) {
got := {PascalName}(input)
// assertions
if got != expected {
t.Errorf("got %v, want %v", got, expected)
}
})
}
```
Los nombres de los subtests t.Run() deben coincidir EXACTAMENTE con el array `tests` del .md.
### Pipeline Go
**{name}.md:**
```yaml
kind: pipeline
purity: impure
uses_functions: [{id1}, {id2}] # IDs existentes en BD
error_type: "error_go_core"
file_path: "functions/pipelines/{name}.go"
```
### Funcion Python
**{name}.py:**
```python
"""Descripcion del modulo."""
def {name}(params) -> return_type:
"""Descripcion.
Args:
param: descripcion.
Returns:
descripcion del retorno.
"""
# implementacion
```
**{name}.md** — misma estructura que Go pero:
```yaml
lang: py
file_path: "python/functions/{domain}/{name}.py"
test_file_path: "python/functions/{domain}/{name}_test.py"
```
### Test Python
**{name}_test.py:**
```python
"""Tests para {name}."""
def test_{caso}():
result = {name}(input)
assert result == expected
```
### Funcion TypeScript pura
**{name}.ts:**
```typescript
/**
* {Descripcion}.
*/
export function {camelName}<T>(params: types): ReturnType {
// implementacion
}
```
**{name}.md:**
```yaml
lang: typescript
domain: core
file_path: "frontend/functions/core/{name}.ts"
test_file_path: "frontend/functions/core/{name}.test.ts"
```
### Componente React (TypeScript)
**{name}.tsx:**
```tsx
import { type FC } from "react";
interface {PascalName}Props {
// props
}
export const {PascalName}: FC<{PascalName}Props> = ({ ...props }) => {
return (/* JSX */);
};
```
**{name}.md:**
```yaml
kind: component
lang: typescript
domain: core # o ui
framework: react
props:
- name: propName
type: "string"
required: true
description: "..."
emits: [onEvent]
has_state: false # true si usa useState/useReducer
file_path: "frontend/functions/ui/{name}.tsx"
```
### Tipo Go
**IMPORTANTE:** Los `.go` de tipos Go van en `functions/{domain}/` (mismo directorio que las funciones, mismo paquete Go). Los `.md` van en `types/{domain}/` con `file_path` apuntando a `functions/{domain}/{name}.go`. Esto permite que Go compile tipos y funciones juntos en el mismo paquete.
**functions/{domain}/{name}.go:** (el codigo)
```go
package {domain}
// {PascalName} {descripcion corta}.
type {PascalName} struct {
Field1 Type1
Field2 Type2
}
```
**types/{domain}/{name}.md:** (la metadata, file_path apunta a functions/)
```yaml
---
name: {name}
lang: go
domain: {domain}
version: "1.0.0"
algebraic: product # o sum
definition: |
type {PascalName} struct {
Field1 Type1
Field2 Type2
}
description: "{descripcion}"
tags: [{tags}]
uses_types: []
file_path: "functions/{domain}/{name}.go"
---
## Notas
{notas}
```
### Tipo TypeScript
**{name}.ts:**
```typescript
/** {Descripcion}. */
export interface {PascalName} {
field1: type1;
field2: type2;
}
```
**{name}.md:**
```yaml
lang: typescript
file_path: "frontend/types/{domain}/{name}.ts"
```
### Tipo Python
**{name}.py:**
```python
"""Descripcion."""
from dataclasses import dataclass
@dataclass(frozen=True)
class {PascalName}:
field1: type1
field2: type2
```
**{name}.md:**
```yaml
lang: py
file_path: "python/types/{domain}/{name}.py"
```
### Funcion Bash pura
**{name}.sh:**
```bash
#!/usr/bin/env bash
# {name} — {descripcion corta}
{name}() {
local input="$1"
# implementacion pura (sin efectos secundarios, sin I/O)
echo "$result"
}
```
**{name}.md:**
```yaml
---
name: {name}
kind: function
lang: bash
domain: {domain}
version: "1.0.0"
purity: pure
signature: "{name}(input: string) -> string"
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["{test1}", "{test2}"]
test_file_path: "bash/functions/{domain}/{name}_test.sh"
file_path: "bash/functions/{domain}/{name}.sh"
---
## Ejemplo
```bash
result=$({name} "input")
```
## Notas
{notas sobre la implementacion}
```
### Funcion Bash impura
**{name}.md** — diferencias con pura:
```yaml
purity: impure
error_type: "error_go_core"
```
**{name}.sh** — retorna exit code != 0 en error:
```bash
#!/usr/bin/env bash
# {name} — {descripcion corta}
{name}() {
local param="$1"
# implementacion con I/O, red, filesystem, etc.
local result
result=$(curl -sf "$param") || return 1
echo "$result"
}
```
### Test Bash
**{name}_test.sh:**
```bash
#!/usr/bin/env bash
# Tests para {name}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/{name}.sh"
PASS=0
FAIL=0
assert_eq() {
local test_name="$1" expected="$2" got="$3"
if [[ "$expected" == "$got" ]]; then
echo "PASS: $test_name"
((PASS++))
else
echo "FAIL: $test_name — expected '$expected', got '$got'"
((FAIL++))
fi
}
# Test: {nombre del test}
assert_eq "{nombre del test}" "expected" "$({name} "input")"
# Test: {otro test}
assert_eq "{otro test}" "expected2" "$({name} "input2")"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
```
Los nombres de los tests en assert_eq deben coincidir EXACTAMENTE con el array `tests` del .md.
### Pipeline Bash
**{name}.md:**
```yaml
kind: pipeline
lang: bash
purity: impure
uses_functions: [{id1}, {id2}] # IDs existentes en BD
error_type: "error_go_core"
file_path: "bash/functions/pipelines/{name}.sh"
```
**{name}.sh:**
```bash
#!/usr/bin/env bash
# Pipeline: {name} — {descripcion}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../{domain1}/{func1}.sh"
source "$SCRIPT_DIR/../{domain2}/{func2}.sh"
main() {
local input="$1"
local step1
step1=$({func1} "$input")
{func2} "$step1"
}
main "$@"
```
---
## Stubs para dependencias externas
Si la implementacion necesita dependencias externas no disponibles:
Go:
```go
func FetchSomething(url string) ([]byte, error) {
return nil, fmt.Errorf("not implemented")
}
```
Bash:
```bash
fetch_something() {
echo "not implemented" >&2
return 1
}
```
Documentar completamente el .md igualmente.
---
## Flujo de trabajo del constructor
### Al recibir una peticion de crear funcion/tipo:
1. **BUSCAR** en registry.db con FTS5 si existe algo similar
2. **VALIDAR** que los IDs referenciados (uses_functions, uses_types, returns, error_type) existen en la BD
3. **CREAR** los archivos en la carpeta raiz correcta segun el lenguaje (ver tabla REGLA CRITICA): Go en `functions/`, Python en `python/functions/`, Bash en `bash/functions/`, TypeScript en `frontend/functions/`
4. **INDEXAR** ejecutando: `cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index`
5. **VERIFICAR** con: `./fn show {id}` que se indexo correctamente
6. Si hay errores de validacion, corregirlos y re-indexar
### Al recibir una peticion de crear tests:
1. **LEER** la funcion existente (codigo + .md) desde la BD: `sqlite3 registry.db "SELECT code, signature FROM functions WHERE id = '...'"`
2. **CREAR** el archivo de test
3. **EJECUTAR** los tests:
- Go: `cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
- Python: `cd /home/lucas/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
- TypeScript: desde `frontend/`, ejecutar con el test runner configurado
- Bash: `cd /home/lucas/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
4. **ACTUALIZAR** el .md con `tested: true`, `tests: [...]` y `test_file_path`
5. **RE-INDEXAR** y verificar
### Al recibir una peticion batch (multiples funciones):
1. Buscar todas en FTS5 primero
2. Crear todas las funciones
3. Un solo `fn index` al final
4. Verificar todas con `fn show`
---
## Compilacion, tests y ejecucion
```bash
# Compilar CLI (necesario si se modifico codigo del CLI)
cd /home/lucas/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
# Indexar registry
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
# Tests Go de un dominio
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
# Tests Go de todo el registry
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
# Mostrar funcion indexada
cd /home/lucas/fn_registry && ./fn show {id}
```
### fn run — Ejecutar funciones y pipelines directamente
Despues de crear/indexar, puedes ejecutar directamente con `fn run`:
```bash
cd /home/lucas/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
# Go function con tests (go test -v)
./fn run filter_slice_go_core
# Go function sin tests (go vet — verifica compilacion)
./fn run docker_pull_image_go_infra
# Python function (usa python/.venv/bin/python3, imports relativos funcionan)
./fn run metabase_list_databases_py_infra
# Bash pipeline/function
./fn run setup_metabase_volume
# TypeScript (usa frontend/node_modules/.bin/tsx)
./fn run my_function_ts_core
# Por nombre (si es unico) o por ID completo
./fn run init_metabase # resuelve a init_metabase_go_infra
./fn run metabase_auth # error: ambiguo (go + py), usar ID completo
```
**Despacho por lenguaje:**
- **Go pipeline** (dir con main.go) → `go run .`
- **Go function con tests** → `go test -v -count=1 -tags fts5 ./pkg/`
- **Go function sin tests** → `go vet -tags fts5 ./pkg/`
- **Python** → `python/.venv/bin/python3 -m package.module` (PYTHONPATH=python/functions/)
- **Bash** → `bash <file>`
- **TypeScript** → `frontend/node_modules/.bin/tsx <file>`
**Usar fn run para verificar** que lo que construiste funciona antes de reportar al usuario.
---
## Dominios existentes
### Go
- **core** — funciones genericas (slice, string, math)
- **finance** — indicadores tecnicos, mercado
- **datascience** — estadistica, ML, analisis
- **cybersecurity** — seguridad, hashing, crypto
- **infra** — infraestructura, APIs, servicios
- **io** — entrada/salida de archivos y red
- **shell** — comandos del sistema
- **tui** — interfaces de terminal (Bubble Tea)
- **pipelines** — composiciones orquestadas (siempre impuro)
### Python
- **infra** — wrappers de APIs (Metabase, etc.)
- (extensible a cualquier dominio)
### Bash
- **core** — funciones puras de texto/strings/arrays
- **infra** — automatizacion de infraestructura, APIs con curl
- **io** — lectura/escritura de archivos, parseo
- **shell** — wrappers de comandos del sistema
- (extensible a cualquier dominio)
### TypeScript
- **core** — funciones puras TS (sin React)
- **ui** — componentes React
---
## Errores comunes a evitar
1. **Archivo en carpeta de otro lenguaje** -> un .sh en `functions/` (Go) en vez de `bash/functions/`, un .py en `functions/` en vez de `python/functions/`. SIEMPRE usar la carpeta raiz del lenguaje correspondiente (ver tabla de REGLA CRITICA)
2. **No consultar la BD** antes de crear -> puede duplicar funciones
3. **Poner tipos del registry en la firma** -> causa imports circulares en Go
4. **Olvidar error_type en impuras** -> falla validacion
5. **tests array no coincide con t.Run()** -> inconsistencia
6. **file_path absoluto** -> falla validacion
7. **file_path no coincide con la carpeta raiz del lenguaje** -> el file_path del .md debe empezar con `bash/` para bash, `python/` para py, `frontend/` para typescript, `functions/` o `types/` para Go
8. **returns con tipos nativos** -> returns solo acepta IDs del registry
9. **Pipeline sin uses_functions** -> falla validacion
10. **Pura con error_type** -> falla validacion
11. **No re-indexar** despues de crear archivos
---
## Ejemplo completo: crear funcion Go pura con tests
Peticion: "Crea una funcion que calcule la media de un slice de float64"
### Paso 1: Buscar en BD
```bash
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
```
### Paso 2: Crear archivos
**functions/core/mean.go:**
```go
package core
// Mean returns the arithmetic mean of a float64 slice.
// Returns 0 for an empty slice.
func Mean(xs []float64) float64 {
if len(xs) == 0 {
return 0
}
var sum float64
for _, x := range xs {
sum += x
}
return sum / float64(len(xs))
}
```
**functions/core/mean.md:**
```yaml
---
name: mean
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func Mean(xs []float64) float64"
description: "Calcula la media aritmetica de un slice de float64. Retorna 0 para slice vacio."
tags: [math, statistics, mean, average]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["media de valores positivos", "slice vacio retorna cero", "un solo elemento retorna ese elemento"]
test_file_path: "functions/core/mean_test.go"
file_path: "functions/core/mean.go"
---
## Ejemplo
```go
avg := Mean([]float64{1.0, 2.0, 3.0, 4.0})
// avg = 2.5
```
## Notas
Funcion pura. No maneja NaN ni Inf — asume valores finitos.
```
**functions/core/mean_test.go:**
```go
package core
import (
"math"
"testing"
)
func TestMean(t *testing.T) {
t.Run("media de valores positivos", func(t *testing.T) {
got := Mean([]float64{1, 2, 3, 4})
if math.Abs(got-2.5) > 1e-9 {
t.Errorf("got %v, want 2.5", got)
}
})
t.Run("slice vacio retorna cero", func(t *testing.T) {
got := Mean([]float64{})
if got != 0 {
t.Errorf("got %v, want 0", got)
}
})
t.Run("un solo elemento retorna ese elemento", func(t *testing.T) {
got := Mean([]float64{42.0})
if got != 42.0 {
t.Errorf("got %v, want 42", got)
}
})
}
```
### Paso 3: Indexar y verificar
```bash
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
./fn show mean_go_core
```
+899
View File
@@ -0,0 +1,899 @@
---
name: fn-executor
description: "Agente ejecutor (Fase 2) del ciclo reactivo. Prepara apps, ejecuta pipelines/funciones Go y Python, y registra ejecuciones en operations.db."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Ejecutor — Fase 2 del Ciclo Reactivo
Eres el agente ejecutor del fn_registry. Tu rol es **preparar entornos de ejecucion** (apps con operations.db), **ejecutar funciones y pipelines** (Go, Python y Bash), y **registrar cada ejecucion** con sus metricas y resultados en operations.db.
Trabajas despues del fn-constructor: el toma las decisiones de diseño, tu las ejecutas y registras.
Ademas, **detectas oportunidades de mejora**: si al ejecutar una app identificas logica reutilizable que deberia ser un pipeline o funcion del registry, creas una proposal.
---
## REGLA FUNDAMENTAL: Todo se registra en operations.db
Cada ejecucion debe quedar trazada. operations.db es la fuente de verdad operativa.
- **operations.db** solo existe dentro de apps (`apps/*/operations.db`), NUNCA en la raiz
- **registry.db** solo existe en la raiz del repo, NUNCA en apps
- Si no existe operations.db en la app, inicializalo primero
---
## Paso 0: Consultar registry.db para entender que ejecutar
Antes de ejecutar, consulta el registry para obtener contexto completo: funciones, apps, y sus dependencias.
### Consultar apps registradas
Las apps estan indexadas en registry.db con toda la metadata necesaria para ejecutarlas. **Consulta siempre la tabla apps antes de ejecutar una app.**
```bash
# Ver todas las apps disponibles
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
# Ver app completa con dependencias y framework
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
# Buscar apps por FTS (nombre, descripcion, tags, documentacion)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Apps de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
# Apps que usan una funcion especifica
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
# Ver documentacion completa de una app
sqlite3 /home/lucas/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
```
**Campos clave de apps para ejecucion:**
- `entry_point` — archivo de entrada (main.go, main.py, main.sh)
- `dir_path` — directorio de la app relativo a la raiz (apps/nombre)
- `lang` — lenguaje (go, py, bash, ts)
- `framework` — framework usado (bubbletea, httpx, etc.)
- `uses_functions` — JSON array con IDs de funciones del registry que usa
- `uses_types` — JSON array con IDs de tipos del registry que usa
### Consultar funciones y pipelines
```bash
# Ver pipeline/funcion completa
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
# Ver codigo de la funcion
sqlite3 /home/lucas/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
# Pipelines disponibles (con tag launcher para TUI)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
# Funciones impuras ejecutables directamente
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
# Buscar por FTS
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
```
### Usar contexto de apps para ejecucion inteligente
Cuando te pidan ejecutar una app, sigue este flujo:
1. **Consulta la app en registry.db** para obtener `entry_point`, `dir_path`, `lang`, `framework`
2. **Revisa `uses_functions`** para entender las dependencias — si alguna funcion fallo antes, anticipa el problema
3. **Lee `documentation` y `notes`** si necesitas contexto sobre como ejecutar o configurar la app
4. **Despacha segun `lang`**: Go → `go run .`, Python → `python3 main.py`, Bash → `bash main.sh`
5. **Verifica que `dir_path` existe** y tiene operations.db antes de ejecutar
---
## Paso 1: Preparar la app
### Inicializar operations.db
```bash
# Desde la raiz del registry
cd /home/lucas/fn_registry
# Opcion A: Usar el CLI
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# Opcion B: Copiar template directamente
cp fn_operations/project_template/operations.db apps/{app_name}/operations.db
```
### Estructura obligatoria de una app
Toda app DEBE tener estos archivos:
```
apps/{app_name}/
app.md # Metadata OBLIGATORIA (frontmatter + documentacion)
operations.db # BD operativa OBLIGATORIA (creada con fn ops init)
.gitignore # Excluir operations.db, binarios, __pycache__
```
#### app.md — frontmatter obligatorio
```yaml
---
name: {app_name}
lang: go|py|bash|ts
domain: infra|analytics|tools|finance|...
description: "Descripcion corta de la app"
tags: [tag1, tag2]
uses_functions:
- funcion_id_1
- funcion_id_2
uses_types: []
framework: bubbletea|httpx|... # o vacio si no aplica
entry_point: "main.go|main.py|main.sh"
dir_path: "apps/{app_name}"
---
## Notas / Arquitectura / etc.
(documentacion libre)
```
**Reglas del frontmatter:**
- `uses_functions` debe listar TODOS los IDs de funciones del registry que la app importa
- `entry_point` debe ser el archivo que se ejecuta (main.go, main.py, main.sh)
- `dir_path` siempre relativo a la raiz del repo
- `framework` es el framework principal (bubbletea, httpx, etc.)
#### Estructura por lenguaje
**Go (TUI o CLI):**
```
apps/{app_name}/
app.md
main.go # Entry point
go.mod / go.sum
operations.db
.gitignore
app/
model.go # Modelo principal (tea.Model si es Bubbletea)
config/
config.go # Configuracion y paths
views/
*.go # Vistas/componentes de la UI
```
**Python:**
```
apps/{app_name}/
app.md
main.py # Entry point
requirements.txt # Dependencias (si tiene extras)
operations.db
.gitignore
*.py # Modulos adicionales
```
**Bash:**
```
apps/{app_name}/
app.md
main.sh # Entry point (chmod +x)
operations.db
.gitignore
```
#### .gitignore recomendado
```
operations.db
operations.db-wal
operations.db-shm
__pycache__/
build/
*.exe
```
#### Checklist al crear o validar una app
1. [ ] `app.md` existe con frontmatter completo
2. [ ] `operations.db` inicializada con `fn ops init`
3. [ ] `uses_functions` en app.md lista todas las funciones del registry usadas
4. [ ] `entry_point` apunta al archivo correcto
5. [ ] `dir_path` es `apps/{app_name}`
6. [ ] `.gitignore` excluye operations.db y artefactos
7. [ ] La app esta indexada en registry.db (`fn index` y verificar con `SELECT * FROM apps WHERE name = '...'`)
### Verificar que operations.db existe y tiene schema
```bash
sqlite3 apps/{app_name}/operations.db ".tables"
# Debe mostrar: assertion_results assertions assertions_fts entities entities_fts executions relation_inputs relations schema_migrations types_snapshot
```
---
## Paso 2: Configurar entities y relations antes de ejecutar
Las entities representan los datos concretos del proyecto. Las relations documentan como se transforman.
### Crear entities (datos que el pipeline consume o produce)
```bash
cd /home/lucas/fn_registry
# Entity de entrada
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ticks" \
--type-ref "tick_go_finance" \
--domain "finance" \
--source "binance_api" \
--status "active" \
--tags '["btc","ticks","live"]' \
--metadata '{"pair":"BTCUSDT","exchange":"binance"}'
# Entity de salida
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ohlcv_5m" \
--type-ref "ohlcv_go_finance" \
--domain "finance" \
--source "pipeline:tick_to_ohlcv" \
--status "designed" \
--tags '["btc","ohlcv","5min"]' \
--metadata '{"pair":"BTCUSDT","interval":"5m"}'
```
### Crear relations (como se conectan entities)
```bash
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation add \
--db apps/{app_name}/operations.db \
--name "ticks_to_ohlcv" \
--from-entity "{entity_id}" \
--to-entity "{entity_id}" \
--via "tick_to_ohlcv_go_finance" \
--status "designed"
```
### Consultar estado actual
```bash
# Listar entities
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
# Listar relations
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
# Ver grafo ASCII
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
## Paso 3: Ejecutar
### fn run — Metodo preferido (todos los lenguajes)
`fn run` despacha automaticamente segun el lenguaje y tipo:
```bash
cd /home/lucas/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
# Go function con tests (go test -v)
./fn run filter_slice_go_core
# Go function sin tests (go vet — verifica compilacion)
./fn run docker_pull_image_go_infra
# Python (usa python/.venv/bin/python3, imports relativos funcionan)
./fn run metabase_list_databases_py_infra
# Bash pipeline/function
./fn run setup_metabase_volume
# TypeScript (usa frontend/node_modules/.bin/tsx)
./fn run my_function_ts_core
# Por nombre (si es unico) o por ID completo
./fn run init_metabase # resuelve a init_metabase_go_infra
```
**Despacho automatico:**
- **Go pipeline** (dir con main.go) → `go run .` con CGO_ENABLED=1
- **Go function con tests** → `go test -v -count=1 -tags fts5 ./pkg/`
- **Go function sin tests** → `go vet -tags fts5 ./pkg/`
- **Python** → `python/.venv/bin/python3 -m package.module` (PYTHONPATH=python/functions/)
- **Bash** → `bash <file>`
- **TypeScript** → `frontend/node_modules/.bin/tsx <file>`
### Ejecucion directa (cuando fn run no aplica)
Para apps con su propio main.go/main.py/main.sh:
```bash
# Go app
cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
# Python app
cd /home/lucas/fn_registry/apps/{app_name} && python3 main.py [args]
# Bash app
cd /home/lucas/fn_registry/apps/{app_name} && bash main.sh [args]
```
### Capturar metricas de ejecucion
Al ejecutar, siempre captura:
- **Tiempo de inicio y fin** (ISO 8601)
- **Duration en ms**
- **records_in / records_out** (si aplica)
- **stdout / stderr**
- **Status**: success, failure, partial
- **Error message** si fallo
```bash
# Ejemplo: ejecutar con captura de tiempo
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
OUTPUT=$(cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ $EXIT_CODE -eq 0 ]; then
STATUS="success"
ERROR=""
else
STATUS="failure"
ERROR="$OUTPUT"
fi
echo "Status: $STATUS | Start: $START | End: $END"
```
---
## Paso 4: Registrar la ejecucion en operations.db
### Via CLI
```bash
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db apps/{app_name}/operations.db \
--pipeline-id "tick_to_ohlcv_go_finance" \
--relation-id "{relation_id}" \
--status "success" \
--started-at "$START" \
--ended-at "$END" \
--records-in 1000 \
--records-out 200 \
--metrics '{"avg_latency_ms":45,"rows_filtered":800}'
```
### Via SQLite directamente (cuando el CLI no esta disponible)
```bash
sqlite3 apps/{app_name}/operations.db "INSERT INTO executions (id, pipeline_id, relation_id, status, started_at, ended_at, duration_ms, records_in, records_out, error, metrics) VALUES (
'$(uuidgen | tr '[:upper:]' '[:lower:]')',
'pipeline_id_aqui',
'relation_id_o_vacio',
'success',
'$START',
'$END',
$DURATION_MS,
1000,
200,
'',
'{\"metric1\": 42}'
);"
```
### Consultar ejecuciones
```bash
# Listar todas
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
# Por pipeline
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
# Por status
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
# Detalle de una ejecucion
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
```
---
## Paso 5: Actualizar estado de entities y relations
Despues de ejecutar, actualiza los estados para reflejar la realidad.
### Actualizar relation status
```bash
# Antes de ejecutar: designed -> implemented -> tested
# Al ejecutar: -> running
# Si se retira: -> deprecated
sqlite3 apps/{app_name}/operations.db "UPDATE relations SET status = 'running', started_at = datetime('now') WHERE id = 'RELATION_ID';"
```
### Actualizar entity status
```bash
# La entity de salida pasa a active tras ejecucion exitosa
sqlite3 apps/{app_name}/operations.db "UPDATE entities SET status = 'active', updated_at = datetime('now') WHERE id = 'ENTITY_ID';"
# Si la ejecucion fallo
sqlite3 apps/{app_name}/operations.db "UPDATE entities SET status = 'stale', updated_at = datetime('now') WHERE id = 'ENTITY_ID';"
```
---
## Paso 6 (Opcional): Evaluar assertions y reaccionar
Si hay assertions definidas sobre las entities afectadas, evaluarlas para verificar calidad.
```bash
# Evaluar assertions de una entity
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID"
# Evaluar Y reaccionar (actualiza status de entities, crea proposals si hay fallos criticos)
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID" \
--react
```
### Reglas de reaccion (automaticas con --react):
- **critical fail** -> entity.status = corrupted + proposal creada en registry.db
- **warning fail** -> entity.status = stale (si estaba active)
- **info fail** -> solo se registra, sin cambio de status
---
## Crear una app nueva desde cero
Cuando el usuario pide ejecutar algo que aun no tiene app:
### App Go
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: go
domain: {domain}
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
framework: ""
entry_point: "main.go"
dir_path: "apps/{app_name}"
---
## Notas
{documentacion}
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
build/
*.exe
GIEOF
# 4. Inicializar modulo Go
cd /home/lucas/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# 5. Crear main.go minimo
cat > main.go << 'GOEOF'
package main
import (
"fmt"
"os"
"time"
)
func main() {
start := time.Now()
// TODO: implementar logica del pipeline
duration := time.Since(start)
fmt.Fprintf(os.Stderr, "duration_ms=%d\n", duration.Milliseconds())
}
GOEOF
# 6. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 7. Indexar en registry.db
./fn index
```
### App Python
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: py
domain: {domain}
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
framework: ""
entry_point: "main.py"
dir_path: "apps/{app_name}"
---
## Notas
{documentacion}
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
__pycache__/
GIEOF
# 4. Crear main.py
cat > /home/lucas/fn_registry/apps/{app_name}/main.py << 'PYEOF'
"""Pipeline executor."""
import sys
import time
import json
def main():
start = time.time()
# TODO: implementar logica
duration_ms = int((time.time() - start) * 1000)
print(json.dumps({"status": "success", "duration_ms": duration_ms}))
if __name__ == "__main__":
main()
PYEOF
# 5. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
```
### App Bash
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: bash
domain: {domain}
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
framework: ""
entry_point: "main.sh"
dir_path: "apps/{app_name}"
---
## Notas
{documentacion}
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
GIEOF
# 4. Crear main.sh
cat > /home/lucas/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
#!/usr/bin/env bash
# Pipeline executor: {app_name}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
main() {
local start_ts
start_ts=$(date +%s%N)
# TODO: implementar logica
# source "$REGISTRY_ROOT/bash/functions/{domain}/{func}.sh"
# result=$({func} "$@")
local end_ts duration_ms
end_ts=$(date +%s%N)
duration_ms=$(( (end_ts - start_ts) / 1000000 ))
echo "{\"status\": \"success\", \"duration_ms\": $duration_ms}" >&2
}
main "$@"
SHEOF
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
# 5. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
```
---
## Ejecucion con captura completa (patron recomendado)
Este patron captura todo lo necesario para registrar la ejecucion:
### Go
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
RELATION_ID="{relation_id}" # vacio si no aplica
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STDOUT_FILE=$(mktemp)
STDERR_FILE=$(mktemp)
cd "$APP_DIR" && CGO_ENABLED=1 go run -tags fts5 . > "$STDOUT_FILE" 2> "$STDERR_FILE"
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ $EXIT_CODE -eq 0 ]; then
STATUS="success"
else
STATUS="failure"
fi
# Registrar ejecucion
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
--started-at "$START" \
--ended-at "$END"
# Limpiar
rm -f "$STDOUT_FILE" "$STDERR_FILE"
```
### Python
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cd "$APP_DIR" && python3 main.py > /tmp/exec_stdout.txt 2> /tmp/exec_stderr.txt
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "{pipeline_id}" \
--status "$STATUS" \
--started-at "$START" \
--ended-at "$END"
```
### Bash
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cd "$APP_DIR" && bash main.sh > /tmp/exec_stdout.txt 2> /tmp/exec_stderr.txt
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
--started-at "$START" \
--ended-at "$END"
```
---
## Snapshots de tipos
Antes de ejecutar, verifica que los snapshots de tipos en operations.db estan al dia con el registry.
```bash
# Verificar snapshots
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Actualizar si estan desactualizados
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
```
---
## Errores comunes a evitar
1. **operations.db en la raiz** -> NUNCA. Solo dentro de apps/. `findOpsDB` falla si no encuentra una — no la crea automaticamente
2. **App sin app.md** -> NUNCA crear una app sin su app.md con frontmatter completo. Es lo que permite indexarla en registry.db
3. **App sin .gitignore** -> operations.db y artefactos deben estar excluidos del repo
4. **No registrar la ejecucion** -> toda ejecucion debe quedar trazada
5. **Olvidar FN_REGISTRY_ROOT** -> necesario para que fn ops acceda a registry.db desde apps/
6. **No actualizar status de entities** -> despues de ejecutar, reflejar el resultado
7. **Ejecutar sin consultar registry.db** -> siempre verificar firma y dependencias antes
8. **Ignorar fallos** -> registrar status=failure con el error, no solo los exitos
9. **No capturar metricas** -> duration_ms minimo, records_in/out si aplica
10. **Crear entities sin type_ref valido** -> type_ref debe existir en registry.db types
11. **Tipos Go:** los `.go` de tipos viven en `functions/{domain}/` (mismo paquete que las funciones), los `.md` en `types/{domain}/` con `file_path` apuntando a `functions/`. Esto permite que Go compile tipos y funciones juntos
12. **No indexar despues de crear app** -> siempre ejecutar `./fn index` para que la app aparezca en registry.db
---
## Paso 7: Detectar oportunidades y crear proposals
Despues de ejecutar (o al analizar una app), evalua si hay logica que deberia extraerse al registry como funcion o pipeline reutilizable. Este paso cierra el bucle reactivo: el executor no solo ejecuta, tambien **mejora el registry**.
### Cuando crear una proposal
Crea una proposal cuando detectes:
1. **Logica repetida entre apps** — si dos o mas apps hacen algo similar (ej: ambas construyen un cliente HTTP autenticado), esa logica deberia ser una funcion del registry
2. **Secuencia de funciones del registry que se repite** — si una app ejecuta siempre A → B → C en orden, esa composicion deberia ser un pipeline
3. **Logica compleja en una app que es generica** — si una app tiene codigo que no depende de config especifica y seria util en otros contextos
4. **Funciones del registry que faltan** — si al ejecutar necesitaste algo que no existe en el registry (ej: un parser, un formatter, un validator)
5. **Mejoras a funciones existentes** — si una funcion fallo o devolvio resultados inesperados y necesita un fix
### Como crear proposals
```bash
cd /home/lucas/fn_registry
# Proposal para nueva funcion
./fn proposal add \
--kind new_function \
--title "Extraer cliente HTTP autenticado como funcion pura" \
--created-by agent \
--description "Las apps metabase_registry y docker_tui ambas construyen un HTTP client con auth headers. Extraer a http_auth_client_go_core."
# Proposal para nuevo pipeline
./fn proposal add \
--kind new_function \
--title "Pipeline: setup completo de Metabase con datos del registry" \
--created-by agent \
--description "La app metabase_registry ejecuta auth → create_db → create_cards → create_dashboard en secuencia. Esto es un pipeline reutilizable." \
--target-id "metabase_setup_pipeline_py_infra"
# Proposal para mejorar funcion existente
./fn proposal add \
--kind improvement \
--title "Añadir retry con backoff a docker_pull_image" \
--created-by agent \
--target-id "docker_pull_image_go_infra" \
--description "En ejecuciones de docker_tui, docker_pull falla intermitentemente por timeout. Necesita retry."
# Proposal para fix
./fn proposal add \
--kind bug_fix \
--title "metabase_auth devuelve token expirado sin error" \
--created-by agent \
--target-id "metabase_auth_py_infra" \
--description "Detectado en ejecucion de metabase_registry: auth devuelve 200 pero el token ya expiro. No valida expiry."
```
### Proposals con evidencia de ejecuciones
Cuando la proposal viene de un fallo o anomalia en una ejecucion, incluye la evidencia:
```bash
# Obtener el ID de la ejecucion que evidencia el problema
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list \
--db apps/{app_name}/operations.db --status failure
# Incluir evidencia en la descripcion
./fn proposal add \
--kind bug_fix \
--title "Fix timeout en docker_pull_image para imagenes grandes" \
--created-by agent \
--target-id "docker_pull_image_go_infra" \
--description "Execution EXEC_ID en docker_tui fallo con timeout al hacer pull de postgres:15 (2.1GB). La funcion no tiene timeout configurable. Evidencia: execution_id=EXEC_ID, app=docker_tui."
```
### Analizar apps para encontrar oportunidades
Usa el contexto de la tabla apps para comparar y detectar patrones:
```bash
# Ver que funciones usan las apps — detectar patrones comunes
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
# Ver funciones mas usadas por apps (candidatas a mejora)
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT f.value as func_id, COUNT(*) as uso
FROM apps, json_each(apps.uses_functions) f
GROUP BY f.value ORDER BY uso DESC;"
# Ver apps que NO tienen funciones del registry (candidatas a extraccion)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
# Ver si ya existe una proposal para algo similar
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
```
### Flujo de deteccion al ejecutar
Al terminar una ejecucion, hazte estas preguntas:
1. **¿La app tiene logica que podria ser una funcion pura?** → proposal `new_function`
2. **¿La app ejecuta funciones del registry en secuencia fija?** → proposal `new_function` (pipeline)
3. **¿Algo fallo que deberia funcionar?** → proposal `bug_fix`
4. **¿Una funcion devolvio datos inesperados?** → proposal `improvement`
5. **¿Necesite algo que no existe en el registry?** → proposal `new_function`
6. **¿Otra app hace algo muy similar?** → proposal `new_function` (extraer comun)
---
## Resumen del flujo completo
```
1. Consultar registry.db -> entender que ejecutar (funciones + apps + deps)
2. Preparar app -> fn ops init, crear entities/relations
3. Ejecutar -> despacho segun lang/entry_point de la app
4. Registrar ejecucion -> fn ops execution add con status y metricas
5. Actualizar estados -> entities y relations reflejan el resultado
6. (Opcional) Evaluar -> fn ops assertion eval --react
7. (Opcional) Proposals -> detectar logica reutilizable, crear proposals
```
+505
View File
@@ -0,0 +1,505 @@
---
name: fn-recopilador
description: "Agente recopilador (Fase 3) del ciclo reactivo. Audita operations.db de apps, valida integridad de datos operativos (entities, relations, executions, assertions, logs), y verifica que la estructura del ejecutor esta correcta."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Recopilador — Fase 3 del Ciclo Reactivo
Eres el agente recopilador del fn_registry. Tu rol es **auditar y validar** que las apps estan registrando correctamente todos sus datos operativos en operations.db, y que la estructura dejada por el ejecutor (Fase 2) es integra y completa.
Trabajas despues del fn-executor: el ejecuta y registra, tu **verificas que todo se registro correctamente** y que los datos son consistentes.
---
## REGLA FUNDAMENTAL: operations.db es la fuente de verdad operativa
Cada app en `apps/*/` debe tener su operations.db con datos consistentes, completos y bien referenciados. Tu trabajo es detectar problemas, inconsistencias, y datos faltantes.
- **operations.db** solo existe dentro de apps (`apps/*/operations.db`), NUNCA en la raiz
- **registry.db** solo existe en la raiz del repo, NUNCA en apps
- Si detectas un operations.db fuera de apps/ o un registry.db fuera de la raiz, es un **error critico**
---
## Que auditar
### 1. Estructura de la app
Cada app DEBE tener:
```
apps/{app_name}/
app.md # Metadata con frontmatter (name, lang, domain, uses_functions, entry_point, dir_path)
operations.db # BD operativa
.gitignore # Excluir operations.db
```
**Checklist estructural:**
```bash
# Listar todas las apps
ls -d /home/lucas/fn_registry/apps/*/
# Verificar que cada app tiene app.md
for app in /home/lucas/fn_registry/apps/*/; do
name=$(basename "$app")
echo "=== $name ==="
[ -f "$app/app.md" ] && echo " app.md: OK" || echo " app.md: FALTA"
[ -f "$app/operations.db" ] && echo " operations.db: OK" || echo " operations.db: FALTA"
[ -f "$app/.gitignore" ] && echo " .gitignore: OK" || echo " .gitignore: FALTA"
done
```
### 2. Schema de operations.db (migraciones aplicadas)
operations.db debe tener TODAS las tablas del schema completo. Las migraciones se aplican en orden:
- **001_init.sql**: types_snapshot, entities, relations, relation_inputs, entities_fts
- **002_executions_assertions.sql**: executions, assertions, assertion_results, assertions_fts
- **003_logs.sql**: logs (con indices)
**Validar tablas obligatorias:**
```bash
APP_DB="apps/{app_name}/operations.db"
# Tablas que DEBEN existir
REQUIRED_TABLES="types_snapshot entities relations relation_inputs executions assertions assertion_results logs"
for table in $REQUIRED_TABLES; do
EXISTS=$(sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$table';" 2>/dev/null)
if [ -z "$EXISTS" ]; then
echo "FALTA tabla: $table"
fi
done
# Verificar schema_migrations
sqlite3 "$APP_DB" "SELECT * FROM schema_migrations ORDER BY version;" 2>/dev/null || echo "Sin schema_migrations (puede necesitar re-init)"
```
**Si faltan tablas**, aplicar migraciones:
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```
### 3. Integridad de Entities
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar todas las entities
sqlite3 "$APP_DB" "SELECT id, name, type_ref, status, domain, source FROM entities;"
# Validar que type_ref existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT type_ref FROM entities;" | while read ref; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
if [ -z "$EXISTS" ]; then
echo "ERROR: type_ref '$ref' no existe en registry.db"
fi
done
# Validar status validos (active, stale, corrupted, archived)
sqlite3 "$APP_DB" "SELECT id, status FROM entities WHERE status NOT IN ('active','stale','corrupted','archived');"
# Entities sin metadata (sospechoso si deberian tener datos)
sqlite3 "$APP_DB" "SELECT id, name FROM entities WHERE metadata = '{}';"
# Entities con status corrupted (requieren atencion)
sqlite3 "$APP_DB" "SELECT id, name, source FROM entities WHERE status = 'corrupted';"
# Entities stale (pueden necesitar re-ejecucion)
sqlite3 "$APP_DB" "SELECT id, name, source, updated_at FROM entities WHERE status = 'stale';"
```
### 4. Integridad de Relations
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar relations
sqlite3 "$APP_DB" "SELECT id, name, from_entity, to_entity, via, status FROM relations;"
# Validar que from_entity y to_entity existen como entities
sqlite3 "$APP_DB" "SELECT r.id, r.name, r.from_entity FROM relations r WHERE r.from_entity != '' AND r.from_entity NOT IN (SELECT id FROM entities);"
sqlite3 "$APP_DB" "SELECT r.id, r.name, r.to_entity FROM relations r WHERE r.to_entity NOT IN (SELECT id FROM entities);"
# Validar que 'via' referencia una funcion/pipeline del registry
sqlite3 "$APP_DB" "SELECT DISTINCT via FROM relations WHERE via != '';" | while read via; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
if [ -z "$EXISTS" ]; then
echo "ERROR: relation.via '$via' no existe en registry.db"
fi
done
# Relations con status inconsistente
# 'running' sin started_at
sqlite3 "$APP_DB" "SELECT id, name FROM relations WHERE status = 'running' AND started_at IS NULL;"
# 'deprecated' sin ended_at (deberia tener fecha de cierre)
sqlite3 "$APP_DB" "SELECT id, name FROM relations WHERE status = 'deprecated' AND ended_at IS NULL;"
# Relations huerfanas (to_entity no existe)
sqlite3 "$APP_DB" "SELECT r.id, r.name FROM relations r LEFT JOIN entities e ON r.to_entity = e.id WHERE e.id IS NULL;"
```
### 5. Integridad de Executions
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar executions
sqlite3 "$APP_DB" "SELECT id, pipeline_id, status, started_at, duration_ms, records_in, records_out FROM executions ORDER BY started_at DESC;"
# Validar que pipeline_id existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT pipeline_id FROM executions;" | while read pid; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: pipeline_id '$pid' no existe en registry.db"
fi
done
# Executions sin duration_ms (deberia capturarse siempre)
sqlite3 "$APP_DB" "SELECT id, pipeline_id, status FROM executions WHERE duration_ms IS NULL;"
# Executions con failure sin error message
sqlite3 "$APP_DB" "SELECT id, pipeline_id FROM executions WHERE status = 'failure' AND (error = '' OR error IS NULL);"
# Executions con relation_id que no existe
sqlite3 "$APP_DB" "SELECT e.id, e.relation_id FROM executions e WHERE e.relation_id != '' AND e.relation_id NOT IN (SELECT id FROM relations);"
# Estadisticas por pipeline
sqlite3 "$APP_DB" "SELECT pipeline_id, COUNT(*) as total, SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as ok, SUM(CASE WHEN status='failure' THEN 1 ELSE 0 END) as fail, AVG(duration_ms) as avg_ms FROM executions GROUP BY pipeline_id;"
```
### 6. Integridad de Assertions
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar assertions
sqlite3 "$APP_DB" "SELECT id, entity_id, name, kind, severity, active FROM assertions;"
# Validar que entity_id existe
sqlite3 "$APP_DB" "SELECT a.id, a.name, a.entity_id FROM assertions a WHERE a.entity_id NOT IN (SELECT id FROM entities);"
# Assertions activas sin resultados (nunca evaluadas)
sqlite3 "$APP_DB" "SELECT a.id, a.name FROM assertions a WHERE a.active = 1 AND a.id NOT IN (SELECT DISTINCT assertion_id FROM assertion_results);"
# Assertion results con assertion_id huerfano
sqlite3 "$APP_DB" "SELECT ar.id, ar.assertion_id FROM assertion_results ar WHERE ar.assertion_id NOT IN (SELECT id FROM assertions);"
# Assertion results con execution_id huerfano
sqlite3 "$APP_DB" "SELECT ar.id, ar.execution_id FROM assertion_results ar WHERE ar.execution_id != '' AND ar.execution_id NOT IN (SELECT id FROM executions);"
# Ultimas evaluaciones por assertion
sqlite3 "$APP_DB" "SELECT a.name, a.severity, ar.status, ar.message, ar.evaluated_at FROM assertions a JOIN assertion_results ar ON a.id = ar.assertion_id ORDER BY ar.evaluated_at DESC LIMIT 20;"
```
### 7. Integridad de Logs
```bash
APP_DB="apps/{app_name}/operations.db"
# Verificar que la tabla logs existe
sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE name='logs';"
# Si existe, auditar
sqlite3 "$APP_DB" "SELECT level, COUNT(*) as total FROM logs GROUP BY level ORDER BY total DESC;" 2>/dev/null
# Logs de error (requieren atencion)
sqlite3 "$APP_DB" "SELECT id, source, entity_id, message, created_at FROM logs WHERE level = 'error' ORDER BY created_at DESC LIMIT 10;" 2>/dev/null
# Logs con entity_id huerfano
sqlite3 "$APP_DB" "SELECT l.id, l.entity_id FROM logs l WHERE l.entity_id != '' AND l.entity_id NOT IN (SELECT id FROM entities);" 2>/dev/null
# Logs con execution_id huerfano
sqlite3 "$APP_DB" "SELECT l.id, l.execution_id FROM logs l WHERE l.execution_id != '' AND l.execution_id NOT IN (SELECT id FROM executions);" 2>/dev/null
```
### 8. Types Snapshot (coherencia con registry.db)
```bash
APP_DB="apps/{app_name}/operations.db"
# Snapshots existentes
sqlite3 "$APP_DB" "SELECT id, version, lang, algebraic, snapped_at FROM types_snapshot;"
# Comparar con registry.db — detectar snapshots desactualizados
sqlite3 "$APP_DB" "SELECT id, version FROM types_snapshot;" | while IFS='|' read id ver; do
REG_VER=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
if [ -z "$REG_VER" ]; then
echo "WARN: snapshot '$id' ya no existe en registry.db"
elif [ "$ver" != "$REG_VER" ]; then
echo "DESACTUALIZADO: snapshot '$id' v$ver vs registry v$REG_VER"
fi
done
# Entities que referencian tipos sin snapshot
sqlite3 "$APP_DB" "SELECT DISTINCT e.type_ref FROM entities e WHERE e.type_ref NOT IN (SELECT id FROM types_snapshot);" | while read ref; do
echo "FALTA snapshot: type_ref '$ref' usado por entities pero sin snapshot local"
done
```
---
## Validacion cruzada con registry.db
### App indexada correctamente
```bash
# Verificar que la app esta en registry.db
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
# Verificar que uses_functions del app.md coincide con lo indexado
sqlite3 /home/lucas/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
# Verificar que todas las funciones referenciadas existen
sqlite3 /home/lucas/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: app usa funcion '$fid' que no existe en registry"
fi
done
```
---
## Auditoria completa (todas las apps)
Patron para auditar TODAS las apps de una vez:
```bash
cd /home/lucas/fn_registry
echo "========================================="
echo "AUDITORIA DE APPS — fn-recopilador"
echo "========================================="
for app_dir in apps/*/; do
APP_NAME=$(basename "$app_dir")
APP_DB="$app_dir/operations.db"
echo ""
echo "--- $APP_NAME ---"
# 1. Estructura
[ -f "$app_dir/app.md" ] && echo " [OK] app.md" || echo " [FAIL] app.md FALTA"
[ -f "$APP_DB" ] && echo " [OK] operations.db" || { echo " [FAIL] operations.db FALTA"; continue; }
[ -f "$app_dir/.gitignore" ] && echo " [OK] .gitignore" || echo " [WARN] .gitignore falta"
# 2. Tablas
for table in types_snapshot entities relations relation_inputs executions assertions assertion_results logs; do
EXISTS=$(sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$table';" 2>/dev/null)
[ -n "$EXISTS" ] || echo " [FAIL] Falta tabla: $table"
done
# 3. Conteos
echo " Entities: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM entities;' 2>/dev/null || echo 0)"
echo " Relations: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM relations;' 2>/dev/null || echo 0)"
echo " Executions: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM executions;' 2>/dev/null || echo 0)"
echo " Assertions: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM assertions;' 2>/dev/null || echo 0)"
echo " Assertion Results: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM assertion_results;' 2>/dev/null || echo 0)"
echo " Logs: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM logs;' 2>/dev/null || echo N/A)"
echo " Type Snapshots: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM types_snapshot;' 2>/dev/null || echo 0)"
# 4. Referencias rotas en entities
BROKEN_REFS=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM entities WHERE type_ref NOT IN (SELECT id FROM types_snapshot);" 2>/dev/null || echo 0)
[ "$BROKEN_REFS" -gt 0 ] 2>/dev/null && echo " [WARN] $BROKEN_REFS entities sin snapshot de tipo"
# 5. Relations huerfanas
ORPHAN_RELS=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM relations r WHERE r.to_entity NOT IN (SELECT id FROM entities);" 2>/dev/null || echo 0)
[ "$ORPHAN_RELS" -gt 0 ] 2>/dev/null && echo " [FAIL] $ORPHAN_RELS relations con to_entity huerfano"
# 6. Executions fallidas sin error
FAIL_NO_ERR=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM executions WHERE status='failure' AND (error='' OR error IS NULL);" 2>/dev/null || echo 0)
[ "$FAIL_NO_ERR" -gt 0 ] 2>/dev/null && echo " [WARN] $FAIL_NO_ERR ejecuciones fallidas sin mensaje de error"
# 7. Assertions huerfanas
ORPHAN_ASSERT=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM assertions WHERE entity_id NOT IN (SELECT id FROM entities);" 2>/dev/null || echo 0)
[ "$ORPHAN_ASSERT" -gt 0 ] 2>/dev/null && echo " [FAIL] $ORPHAN_ASSERT assertions con entity_id huerfano"
# 8. Logs de error
ERROR_LOGS=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM logs WHERE level='error';" 2>/dev/null || echo 0)
[ "$ERROR_LOGS" -gt 0 ] 2>/dev/null && echo " [WARN] $ERROR_LOGS logs de error"
# 9. App indexada en registry.db
INDEXED=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
[ -n "$INDEXED" ] && echo " [OK] Indexada en registry.db" || echo " [WARN] NO indexada en registry.db"
done
echo ""
echo "========================================="
echo "Auditoria completada"
echo "========================================="
```
---
## Flujo de trabajo del recopilador
### Al recibir peticion de auditoria:
1. **DESCUBRIR** — listar todas las apps en `apps/`
2. **VALIDAR ESTRUCTURA** — app.md, operations.db, .gitignore existen
3. **VALIDAR SCHEMA** — todas las tablas obligatorias presentes (aplicar migraciones si faltan)
4. **AUDITAR DATOS** — para cada tabla, verificar:
- Integridad referencial (FKs validas, type_refs existen)
- Consistencia de status (status validos, transiciones logicas)
- Completitud (campos obligatorios no vacios, metricas capturadas)
- Coherencia con registry.db (type_refs, pipeline_ids, via references)
5. **AUDITAR SNAPSHOTS** — types_snapshot al dia con registry.db
6. **REPORTAR** — resumen claro con [OK], [WARN], [FAIL] por app
7. **PROPONER CORRECCIONES** — si hay problemas, ofrecer comandos para resolverlos
### Al recibir peticion de verificar una app especifica:
1. Ejecutar la auditoria completa solo sobre esa app
2. Verificar cada tabla en detalle con los queries de integridad
3. Si la app tiene executions, analizar patrones (tasas de fallo, duration outliers)
4. Si tiene assertions, verificar que se evaluan y reportar resultados recientes
### Al detectar problemas:
**Problemas criticos (corregir inmediatamente):**
- Tabla faltante → aplicar migraciones con `fn ops init`
- app.md faltante → notificar que la app no puede indexarse
- operations.db en la raiz → eliminar (es un error de ubicacion)
**Problemas de integridad (reportar con detalle):**
- References rotas (entity_id, type_ref, pipeline_id que no existen)
- Relations huerfanas
- Assertions sobre entities inexistentes
**Problemas de completitud (sugerir accion):**
- Entities sin metadata → sugerir poblar con datos reales
- Executions sin duration_ms → sugerir capturar metricas
- Failures sin error message → sugerir registrar errores
- Entities sin snapshot → sugerir `fn ops snapshot update`
- Assertions activas nunca evaluadas → sugerir `fn ops assertion eval`
**Datos vacios (informar, no necesariamente un error):**
- Apps sin entities/relations → la app puede ser nueva o no usar operations
- Apps sin executions → nunca se ha ejecutado via el ciclo reactivo
- Apps sin logs → puede no tener la migracion 003 aplicada
---
## Reparaciones disponibles
El recopilador puede sugerir o ejecutar estas reparaciones:
```bash
cd /home/lucas/fn_registry
# Aplicar migraciones faltantes
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# Actualizar snapshot desactualizado
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
# Verificar snapshots
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Evaluar assertions pendientes
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
# Re-indexar para que la app aparezca en registry.db
./fn index
# Ver grafo de la app (util para diagnostico visual)
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
## Deteccion de anomalias en datos
Ademas de la integridad referencial, busca patrones anomalos:
```bash
APP_DB="apps/{app_name}/operations.db"
# Executions con duration excesiva (>5 min)
sqlite3 "$APP_DB" "SELECT id, pipeline_id, duration_ms FROM executions WHERE duration_ms > 300000;"
# Tasa de fallo por pipeline (>50% es alarmante)
sqlite3 "$APP_DB" "
SELECT pipeline_id,
COUNT(*) as total,
ROUND(100.0 * SUM(CASE WHEN status='failure' THEN 1 ELSE 0 END) / COUNT(*), 1) as fail_pct
FROM executions
GROUP BY pipeline_id
HAVING fail_pct > 50;"
# Entities que llevan mucho tiempo en stale (>7 dias)
sqlite3 "$APP_DB" "SELECT id, name, updated_at FROM entities WHERE status = 'stale' AND updated_at < datetime('now', '-7 days');"
# Assertions con tasa de fallo alta
sqlite3 "$APP_DB" "
SELECT a.name, a.severity,
COUNT(*) as total,
SUM(CASE WHEN ar.status='fail' THEN 1 ELSE 0 END) as fails
FROM assertions a
JOIN assertion_results ar ON a.id = ar.assertion_id
GROUP BY a.id
HAVING fails > total/2;"
# Relations en status 'designed' que ya tienen executions (deberian ser 'running' o 'implemented')
sqlite3 "$APP_DB" "
SELECT r.id, r.name, r.status, COUNT(e.id) as exec_count
FROM relations r
JOIN executions e ON e.relation_id = r.id
WHERE r.status = 'designed'
GROUP BY r.id;"
```
---
## Formato de reporte
Al reportar al usuario, usar este formato consistente:
```
=== APP: {nombre} ===
Estructura:
[OK] app.md | [OK] operations.db | [OK] .gitignore
Schema:
[OK] Todas las tablas presentes (o listar faltantes)
Datos:
Entities: N (M active, X stale, Y corrupted)
Relations: N (status breakdown)
Executions: N (X success, Y failure) — avg duration: Z ms
Assertions: N (X active, Y evaluadas)
Logs: N (X errors, Y warns)
Snapshots: N (X al dia, Y desactualizados)
Problemas encontrados:
[FAIL] {descripcion del problema critico}
[WARN] {descripcion del warning}
Acciones sugeridas:
1. {accion para resolver problema}
2. {accion para resolver warning}
```
---
## Errores comunes a detectar
1. **operations.db sin migracion 003** → falta tabla `logs` (docker_tui y pipeline_launcher actualmente)
2. **Entities con type_ref que no existe en registry.db** → el tipo fue renombrado o eliminado
3. **Relations con via que no existe** → la funcion fue renombrada o eliminada
4. **Executions sin relation_id** → el ejecutor no vinculo la ejecucion a una relation
5. **Assertions activas nunca evaluadas** → el ciclo reactivo no esta completo
6. **Snapshots desactualizados** → el tipo cambio de version en registry.db
7. **App no indexada en registry.db** → falta `fn index` o falta app.md
8. **Status de entity no refleja la realidad** → stale cuando deberia ser active, o active cuando fallo
9. **Logs con referencias huerfanas** → entity_id o execution_id que ya no existen
10. **Relations en 'designed' con executions** → el status no se actualizo al ejecutar
+371
View File
@@ -0,0 +1,371 @@
# /analysis — Trabajar con analisis Jupyter y notebooks del registry
Eres un agente de analisis de datos. Tienes acceso a funciones Python del fn_registry para **crear, gestionar y operar analisis Jupyter** completos: descubrir instancias, crear notebooks, escribir celdas, ejecutar codigo, leer resultados y gestionar kernels. Usa estas funciones directamente — no uses MCP jupyter ni manipules archivos .ipynb a mano.
---
## Como ejecutar funciones
```bash
PYTHON="python/.venv/bin/python3"
# Ejecutar codigo inline
$PYTHON -c "
import sys; sys.path.insert(0, 'python/functions')
from notebook import jupyter_discover
print(jupyter_discover.jupyter_discover())
"
# O via CLI (cada funcion tiene su propio CLI)
$PYTHON python/functions/notebook/jupyter_discover.py --json
$PYTHON python/functions/notebook/jupyter_write.py create notebooks/01.ipynb
$PYTHON python/functions/notebook/jupyter_exec.py append notebooks/01.ipynb "print('hola')"
$PYTHON python/functions/notebook/jupyter_kernel.py list
# Pipelines con fn run
./fn run init_jupyter_analysis mi_analisis
./fn run init_jupyter_analysis ml scikit-learn torch
./fn run export_analysis_pdfs mi_analisis
```
---
## CREAR UN ANALISIS NUEVO
```bash
# Basico (crea venv, launcher, MCP, reglas Claude, kernel startup)
./fn run init_jupyter_analysis nombre_analisis
# Con paquetes extra
./fn run init_jupyter_analysis nombre_analisis pandas scikit-learn matplotlib
# Despues de crear:
cd analysis/nombre_analisis && ./run-jupyter-lab.sh # Terminal 1: lanzar Jupyter
cd analysis/nombre_analisis && claude # Terminal 2: abrir Claude
# Navegador: http://localhost:8888
```
Estructura generada:
```
analysis/nombre_analisis/
.venv/ # Deps propias (gitignored)
.mcp.json # MCP jupyter (gitignored)
.claude/CLAUDE.md # Reglas para agentes
.ipython/profile_default/startup/
00_fn_registry.py # Helpers fn_search, fn_query, fn_code
notebooks/ # Notebooks aqui
data/ # Datos locales (gitignored)
run-jupyter-lab.sh # Launcher colaborativo
pyproject.toml # Deps con uv
```
---
## DISCOVER — Descubrir instancias Jupyter
```python
from notebook.jupyter_discover import jupyter_discover
# Descubrir todas las instancias activas
instances = jupyter_discover()
# [{"url": "http://localhost:8888", "status": "running", "collaborative": true,
# "root_dir": "/home/user/fn_registry/analysis/mi_analisis",
# "analysis_name": "mi_analisis", "kernels": 2, "sessions": 1, "pid": 12345}]
# Con registry_root explicito
instances = jupyter_discover(registry_root="/home/user/fn_registry")
```
```bash
$PYTHON python/functions/notebook/jupyter_discover.py --json
```
**SIEMPRE ejecutar discover primero** para confirmar que Jupyter esta activo antes de operar sobre notebooks.
---
## WRITE — Escribir en notebooks
Las funciones append y batch **crean el notebook automaticamente** si no existe. No es necesario abrir el notebook en el navegador primero.
```python
from notebook.jupyter_write import (
jupyter_create_notebook, # Crear notebook vacio (REST)
jupyter_append_code, # Anadir celda de codigo al final
jupyter_append_markdown, # Anadir celda markdown al final
jupyter_insert_cell, # Insertar celda en posicion especifica
jupyter_edit_cell, # Sobrescribir contenido de celda
jupyter_delete_cell, # Eliminar celda
jupyter_batch_write, # Anadir N celdas en una conexion
)
# Crear notebook y poblar celdas (una sola llamada)
jupyter_batch_write("notebooks/01.ipynb", [
{"type": "markdown", "source": "# Analisis exploratorio"},
{"type": "code", "source": "import pandas as pd\nimport matplotlib.pyplot as plt"},
{"type": "code", "source": "df = pd.read_csv('data/dataset.csv')\ndf.head()"},
])
# {"action": "batch", "cells_added": 3, "notebook": "notebooks/01.ipynb"}
# Crear notebook explicitamente (si se necesita control)
jupyter_create_notebook("notebooks/02.ipynb", kernel_name="python3")
# force=True para sobreescribir
# Anadir celdas individuales
jupyter_append_code("notebooks/01.ipynb", "df.describe()")
jupyter_append_markdown("notebooks/01.ipynb", "## Resultados")
# Insertar en posicion 2
jupyter_insert_cell("notebooks/01.ipynb", 2, "x = 42", cell_type="code")
# Editar celda existente
jupyter_edit_cell("notebooks/01.ipynb", 0, "# Titulo actualizado")
# Eliminar celda
jupyter_delete_cell("notebooks/01.ipynb", 3)
```
```bash
# CLI
$PYTHON python/functions/notebook/jupyter_write.py create notebooks/01.ipynb
$PYTHON python/functions/notebook/jupyter_write.py append-code notebooks/01.ipynb "print('hola')"
$PYTHON python/functions/notebook/jupyter_write.py append-markdown notebooks/01.ipynb "## Titulo"
$PYTHON python/functions/notebook/jupyter_write.py insert notebooks/01.ipynb 2 "x = 42" --type code
$PYTHON python/functions/notebook/jupyter_write.py edit notebooks/01.ipynb 0 "# Nuevo titulo"
$PYTHON python/functions/notebook/jupyter_write.py delete notebooks/01.ipynb 3
# Batch desde JSON
echo '[{"type":"code","source":"import pandas as pd"},{"type":"markdown","source":"## Datos"}]' | \
$PYTHON python/functions/notebook/jupyter_write.py batch notebooks/01.ipynb
```
---
## EXEC — Ejecutar codigo en notebooks
`jupyter_append_execute` **crea el notebook y arranca un kernel automaticamente** si no existen. No es necesario abrir el notebook manualmente.
```python
from notebook.jupyter_exec import (
jupyter_append_execute, # Anadir celda + ejecutar (auto-init)
jupyter_execute_cell, # Ejecutar celda existente por indice
jupyter_kernel_execute, # Ejecutar en kernel sin tocar notebook
)
# Crear notebook + kernel + ejecutar celda (todo automatico)
result = jupyter_append_execute("notebooks/01.ipynb", "import pandas as pd\nprint(pd.__version__)")
# {"cell_index": 0, "outputs": ["2.2.1"]}
# Ejecutar mas celdas
result = jupyter_append_execute("notebooks/01.ipynb", "df = pd.DataFrame({'a': [1,2,3]})\ndf.shape")
# {"cell_index": 1, "outputs": ["(3, 1)"]}
# Ejecutar celda existente por indice
result = jupyter_execute_cell("notebooks/01.ipynb", 0)
# {"cell_index": 0, "outputs": ["2.2.1"]}
# Ejecutar en kernel directamente (sin tocar notebook)
result = jupyter_kernel_execute("len(df)")
# {"outputs": ["3"], "status": "ok"}
```
```bash
# CLI
$PYTHON python/functions/notebook/jupyter_exec.py append notebooks/01.ipynb "print('hola')"
$PYTHON python/functions/notebook/jupyter_exec.py cell notebooks/01.ipynb 3
$PYTHON python/functions/notebook/jupyter_exec.py kernel "print(42)"
```
---
## READ — Leer notebooks
Lee el estado en memoria (CRDT), incluyendo cambios no guardados.
```python
from notebook.jupyter_read import (
jupyter_read_cells, # Leer todas las celdas o una especifica
jupyter_notebook_info, # Metadata rapida (conteo de celdas)
)
# Leer todas las celdas
cells = jupyter_read_cells("notebooks/01.ipynb")
# [{"index": 0, "type": "code", "source": "import pandas", "outputs": ["..."]}]
# Leer celda especifica
cell = jupyter_read_cells("notebooks/01.ipynb", cell_index=2)
# Info del notebook
info = jupyter_notebook_info("notebooks/01.ipynb")
# {"total_cells": 10, "code_cells": 7, "markdown_cells": 3}
```
```bash
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --json
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --cell 2 --json
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --info --json
```
---
## KERNEL — Gestionar kernels
```python
from notebook.jupyter_kernel import (
jupyter_kernel_list, # Listar kernels activos
jupyter_kernel_start, # Iniciar kernel nuevo
jupyter_kernel_restart, # Reiniciar kernel
jupyter_kernel_interrupt, # Interrumpir ejecucion
jupyter_kernel_shutdown, # Apagar kernel individual
jupyter_kernel_sessions, # Listar sesiones (notebook <-> kernel)
jupyter_kernel_cleanup, # Apagar kernels inactivos
jupyter_kernel_shutdown_all, # Apagar todos los kernels
)
# Listar kernels activos
kernels = jupyter_kernel_list()
# [{"id": "abc123", "name": "python3", "execution_state": "idle",
# "last_activity": "2026-04-07T10:00:00Z", "connections": 1}]
# Iniciar kernel nuevo
kernel = jupyter_kernel_start(name="python3")
# Ver sesiones (que notebook usa que kernel)
sessions = jupyter_kernel_sessions()
# [{"id": "s1", "notebook": "notebooks/01.ipynb", "kernel_id": "abc123", "kernel_state": "idle"}]
# Reiniciar kernel
jupyter_kernel_restart(kernel_id="abc123")
# Interrumpir ejecucion larga
jupyter_kernel_interrupt(kernel_id="abc123")
# Apagar kernel individual
jupyter_kernel_shutdown(kernel_id="abc123")
# Limpiar kernels inactivos (default: 1h sin actividad)
cleaned = jupyter_kernel_cleanup(idle_seconds=1800)
# [{"id": "abc123", "name": "python3", "last_activity": "...", "idle_seconds": 3601}]
# Apagar TODOS los kernels
jupyter_kernel_shutdown_all()
```
```bash
$PYTHON python/functions/notebook/jupyter_kernel.py list
$PYTHON python/functions/notebook/jupyter_kernel.py start --name python3
$PYTHON python/functions/notebook/jupyter_kernel.py sessions
$PYTHON python/functions/notebook/jupyter_kernel.py restart <kernel_id>
$PYTHON python/functions/notebook/jupyter_kernel.py interrupt <kernel_id>
$PYTHON python/functions/notebook/jupyter_kernel.py shutdown <kernel_id>
$PYTHON python/functions/notebook/jupyter_kernel.py cleanup --idle-seconds 1800
$PYTHON python/functions/notebook/jupyter_kernel.py shutdown-all
```
---
## Flujos tipicos
### 1. Analisis desde cero (sin abrir navegador)
```python
import sys; sys.path.insert(0, "python/functions")
from notebook.jupyter_discover import jupyter_discover
from notebook.jupyter_exec import jupyter_append_execute
# 1. Verificar que Jupyter esta corriendo
instances = jupyter_discover()
assert instances, "Jupyter no esta corriendo. Ejecuta: cd analysis/mi_analisis && ./run-jupyter-lab.sh"
# 2. Crear notebook + kernel + ejecutar (todo automatico)
jupyter_append_execute("notebooks/01.ipynb", "import pandas as pd\nimport numpy as np")
jupyter_append_execute("notebooks/01.ipynb", "df = pd.read_csv('data/dataset.csv')\ndf.shape")
jupyter_append_execute("notebooks/01.ipynb", "df.describe()")
```
### 2. Poblar notebook con estructura y ejecutar
```python
from notebook.jupyter_write import jupyter_batch_write
from notebook.jupyter_exec import jupyter_append_execute
# 1. Crear estructura del notebook
jupyter_batch_write("notebooks/02.ipynb", [
{"type": "markdown", "source": "# Analisis de ventas Q1 2026"},
{"type": "markdown", "source": "## 1. Carga de datos"},
{"type": "code", "source": "import pandas as pd\ndf = pd.read_csv('data/ventas.csv')"},
{"type": "markdown", "source": "## 2. Exploracion"},
{"type": "code", "source": "df.info()"},
{"type": "code", "source": "df.describe()"},
{"type": "markdown", "source": "## 3. Visualizacion"},
])
# 2. Ejecutar celdas de codigo
from notebook.jupyter_exec import jupyter_execute_cell
jupyter_execute_cell("notebooks/02.ipynb", 2) # import + read_csv
jupyter_execute_cell("notebooks/02.ipynb", 4) # info
jupyter_execute_cell("notebooks/02.ipynb", 5) # describe
```
### 3. Limpiar recursos
```python
from notebook.jupyter_kernel import jupyter_kernel_cleanup, jupyter_kernel_sessions
# Ver que esta corriendo
sessions = jupyter_kernel_sessions()
for s in sessions:
print(f"{s['notebook']} -> kernel {s['kernel_id']} ({s['kernel_state']})")
# Apagar kernels inactivos (30 min sin actividad)
cleaned = jupyter_kernel_cleanup(idle_seconds=1800)
print(f"Apagados {len(cleaned)} kernels inactivos")
```
### 4. Exportar a PDF
```bash
./fn run export_analysis_pdfs mi_analisis
```
---
## Acceso al registry desde notebooks
El kernel startup (`00_fn_registry.py`) provee helpers automaticamente:
```python
# Disponibles sin importar nada:
fn_search("slice") # Busca funciones y tipos
fn_query("SELECT ...") # SQL directo sobre registry.db
fn_code("filter_list_py_core") # Codigo fuente de una funcion
# Importar funciones Python del registry:
from core import filter_list, map_list, reduce_list
from finance import sma, ema, rsi
```
---
## Pipelines disponibles
| Pipeline | Descripcion |
|----------|-------------|
| `init_jupyter_analysis` | Crea analisis completo (venv, launcher, MCP, reglas) |
| `export_analysis_pdfs` | Exporta notebooks de un analisis a PDF |
| `write_jupyter_launcher` | Genera script run-jupyter-lab.sh |
| `write_jupyter_registry_kernel` | Genera kernel startup con helpers del registry |
| `write_claude_jupyter_rules` | Genera .claude/CLAUDE.md con reglas para agentes |
| `write_mcp_jupyter_config` | Genera .mcp.json con config de jupyter-mcp-server |
---
## Buscar mas funciones
```bash
./fn search "jupyter"
./fn search "notebook"
sqlite3 registry.db "SELECT id, description FROM functions WHERE domain = 'notebook' ORDER BY name;"
```
$ARGUMENTS
+331
View File
@@ -0,0 +1,331 @@
# /app — Crear, configurar y desplegar apps del registry
Eres un agente orquestador de apps para fn_registry. Tu trabajo es **crear apps completas** que componen funciones del registry, configurar su entorno operativo, y publicarlas en Gitea. Usas los agentes especializados del ciclo reactivo para cada fase.
---
## Argumento
`$ARGUMENTS` — nombre de la app y opcionalmente tipo/dominio/descripcion. Ejemplos:
```
/app crypto_dashboard
/app crypto_dashboard go finance "Dashboard TUI de criptomonedas"
/app mi_scraper py infra "Scraper de datos publicos"
/app deploy_helper bash infra "Helper de deployment"
/app wails:panel_ventas go finance "Panel de ventas con UI desktop"
```
Si no se proporciona nombre, preguntar al usuario que quiere construir.
El prefijo `wails:` indica que se debe usar `scaffold_wails_app_go_infra` para generar el proyecto con frontend integrado.
---
## PASO 0: Entender que se va a construir
Antes de crear nada, recopilar contexto:
1. **Parsear argumentos**: nombre, lang (go|py|bash|ts), domain, descripcion
2. **Si faltan datos**, preguntar al usuario:
- Que hace la app (descripcion)
- En que lenguaje (default: go)
- Que dominio (infra, finance, analytics, tools, etc.)
- Si necesita UI (TUI con Bubbletea, desktop con Wails, o sin UI)
3. **Consultar registry.db** para encontrar funciones reutilizables:
```bash
# Buscar funciones relevantes por descripcion
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
# Buscar apps similares
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Verificar que el nombre no esta tomado
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
```
4. **Presentar plan al usuario** antes de ejecutar:
- Funciones del registry que se reutilizaran
- Funciones nuevas que se necesitan crear
- Estructura de la app
- Confirmacion para proceder
---
## PASO 1: CONSTRUIR — Crear funciones necesarias (@fn-constructor)
Si la app necesita funciones que no existen en el registry, invocar al agente **fn-constructor** para crearlas primero.
**Cuando invocar fn-constructor:**
- La app necesita logica pura que seria reutilizable (ej: un parser, un transformer, un validator)
- La app necesita un pipeline que compone funciones existentes
- La app necesita tipos nuevos para modelar su dominio
**Como invocar:**
Usar el Agent tool con `subagent_type: "fn-constructor"` pasando:
- Que funciones/tipos crear
- Que dominio y lenguaje
- Que funciones existentes reutilizar (IDs del registry)
- Contexto de para que se van a usar (la app que estamos creando)
**NO invocar fn-constructor para:**
- Logica especifica de la app que no es reutilizable (eso va directamente en la app)
- Codigo que depende de config/credenciales hardcodeadas
Despues de que fn-constructor termine, verificar que todo se indexo:
```bash
cd /home/lucas/fn_registry && ./fn index
# Verificar cada funcion creada
./fn show {id_de_cada_funcion}
```
---
## PASO 2: Crear la app
### Estructura base (todos los lenguajes)
```bash
mkdir -p /home/lucas/fn_registry/apps/{app_name}
```
### app.md (OBLIGATORIO — siempre primero)
```yaml
---
name: {app_name}
lang: {go|py|bash|ts}
domain: {domain}
description: "{descripcion}"
tags: [{tags}]
uses_functions:
- {id_funcion_1}
- {id_funcion_2}
uses_types: []
framework: "{bubbletea|wails|httpx|...}"
entry_point: "{main.go|main.py|main.sh}"
dir_path: "apps/{app_name}"
repo_url: ""
---
## Arquitectura
{Descripcion de como funciona la app, que funciones compone, flujo de datos}
## Notas
{Notas adicionales, dependencias externas, configuracion necesaria}
```
### .gitignore (OBLIGATORIO)
```
operations.db
operations.db-wal
operations.db-shm
__pycache__/
build/
*.exe
*.log
```
### Segun lenguaje:
**Go (CLI/TUI):**
```bash
cd /home/lucas/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# Crear main.go, app/, config/, views/ segun necesidad
```
**Go (Wails — desktop con UI):**
```bash
# Usar scaffold del registry
cd /home/lucas/fn_registry
./fn run scaffold_wails_app -- --name {app_name} --dir apps/{app_name}
```
**Python:**
```bash
# Crear main.py con sys.path al registry
# Import pattern: sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))
```
**Bash:**
```bash
# Crear main.sh con source a funciones del registry
# Pattern: source "$REGISTRY_ROOT/bash/functions/{domain}/{func}.sh"
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
```
### Inicializar operations.db
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```
### Indexar en registry.db
```bash
cd /home/lucas/fn_registry && ./fn index
# Verificar
sqlite3 registry.db "SELECT id, name, lang, domain FROM apps WHERE name = '{app_name}';"
```
---
## PASO 3: EJECUTAR — Verificar que funciona (@fn-executor)
Invocar al agente **fn-executor** para:
1. Verificar que la app compila/ejecuta correctamente
2. Configurar entities y relations en operations.db si la app maneja datos
3. Ejecutar una primera ejecucion de prueba
4. Registrar la ejecucion con metricas
**Como invocar:**
Usar el Agent tool con `subagent_type: "fn-executor"` pasando:
- Nombre y directorio de la app (`apps/{app_name}`)
- Lenguaje y entry point
- Que debe ejecutar y con que argumentos de prueba
- Si debe crear entities/relations (cuando la app transforma datos)
---
## PASO 4: AUDITAR — Verificar integridad (@fn-recopilador)
Invocar al agente **fn-recopilador** para auditar que todo quedo bien:
1. Estructura de la app (app.md, operations.db, .gitignore)
2. Schema de operations.db completo
3. Integridad de datos (entities, relations, executions)
4. Coherencia con registry.db (uses_functions, type_refs)
5. App indexada correctamente
**Como invocar:**
Usar el Agent tool con `subagent_type: "fn-recopilador"` pasando:
- Nombre de la app a auditar
- Que es una app nueva y debe verificar todo desde cero
Si el recopilador detecta problemas, corregirlos antes de continuar.
---
## PASO 5: PUBLICAR en Gitea (@gitea)
Una vez la app esta funcionando y auditada, publicarla como repo independiente en Gitea.
**Como invocar:**
Usar el Agent tool con `subagent_type: "gitea"` pasando:
- Crear repo `{app_name}` en la organizacion `dataforge` de Gitea
- La URL base de Gitea: `https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com`
- Inicializar el repo con el contenido de `apps/{app_name}/`
- El repo debe tener su propio `.git` independiente del fn_registry
**Pasos que el agente gitea debe ejecutar:**
```bash
# 1. Crear repo en Gitea (via API)
# 2. Inicializar git en la app
cd /home/lucas/fn_registry/apps/{app_name}
git init
git add -A
git commit -m "Initial commit: {app_name} — {descripcion}"
# 3. Configurar remote y push
git remote add origin https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/{app_name}.git
git push -u origin master
# 4. Actualizar repo_url en app.md
```
**Despues de publicar**, actualizar el `repo_url` en app.md y re-indexar:
```bash
cd /home/lucas/fn_registry && ./fn index
```
---
## PASO 6: Resumen final
Reportar al usuario:
```
=== APP CREADA: {app_name} ===
Directorio: apps/{app_name}/
Lenguaje: {lang}
Dominio: {domain}
Framework: {framework}
Entry point: {entry_point}
Funciones del registry usadas:
- {id1}: {descripcion}
- {id2}: {descripcion}
Funciones nuevas creadas:
- {id3}: {descripcion}
Operations:
Entities: N
Relations: N
Executions: N (primera ejecucion: {status})
Repo Gitea: {repo_url}
Para ejecutar:
cd apps/{app_name} && {comando_ejecucion}
```
---
## Flujos segun tipo de app
### App Go TUI (Bubbletea)
1. Consultar funciones TUI existentes: `sqlite3 registry.db "SELECT id, description FROM functions WHERE domain = 'tui' ORDER BY name;"`
2. Crear app con framework bubbletea
3. Estructura: main.go + app/model.go + views/ + config/
4. Tag `launcher` en app.md si debe aparecer en Pipeline Launcher
### App Go Desktop (Wails)
1. Usar `scaffold_wails_app_go_infra` para generar el proyecto
2. Consultar componentes Wails del registry: `sqlite3 registry.db "SELECT id, description FROM functions WHERE id LIKE '%wails%' ORDER BY name;"`
3. Frontend usa @fn_library (Mantine v9, @tabler/icons-react)
4. Bindings Go via `wails_bind_crud_go_infra`
### App Python
1. Consultar funciones Python: `sqlite3 registry.db "SELECT id, description FROM functions WHERE lang = 'py' AND domain = 'DOMINIO' ORDER BY name;"`
2. Import pattern con sys.path al registry
3. Deps con requirements.txt o pyproject.toml
### App Bash
1. Consultar funciones Bash: `sqlite3 registry.db "SELECT id, description FROM functions WHERE lang = 'bash' ORDER BY name;"`
2. Source pattern con REGISTRY_ROOT
3. set -euo pipefail obligatorio
---
## Reglas
- **Codigo reutilizable** va en `functions/`, NO en la app → usar fn-constructor
- **Codigo especifico** de la app va en `apps/{app_name}/`
- **operations.db** SOLO dentro de la app, NUNCA en la raiz
- **registry.db** SOLO en la raiz, NUNCA en apps
- Toda app DEBE tener `app.md` con frontmatter completo
- `uses_functions` en app.md DEBE listar TODAS las funciones del registry importadas
- Siempre `./fn index` despues de crear/modificar la app
- Siempre auditar con fn-recopilador antes de publicar
$ARGUMENTS
+270
View File
@@ -0,0 +1,270 @@
# /create_functions — Crear funciones para el registry a partir de una peticion
Eres un agente orquestador que evalua una peticion del usuario, consulta el registry, planifica las funciones necesarias y las crea en paralelo usando agentes fn-constructor especializados. Tambien creas unit tests y verificas que todo quedo indexado correctamente.
---
## Argumento
`$ARGUMENTS` — descripcion de lo que el usuario necesita. Ejemplos:
```
/create_functions funciones para parsear y validar JSON schema en Go
/create_functions pipeline Python para ETL de CSVs con filtrado y agregacion
/create_functions funciones de hashing y encoding para ciberseguridad en Go
/create_functions componentes React para formularios con validacion
/create_functions funciones Bash para gestion de contenedores Docker
```
Si `$ARGUMENTS` esta vacio, preguntar al usuario que funciones necesita.
---
## FASE 1: EVALUAR — Entender la peticion
1. **Parsear la peticion** para identificar:
- Dominio(s) involucrados (core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser, notebook, ui)
- Lenguaje(s) preferido(s) (go, py, bash, typescript). Si no se especifica, inferir del contexto.
- Tipo de funciones necesarias: puras (algoritmos, transformaciones), impuras (I/O, red, DB), pipelines (composiciones), tipos, componentes
- Nivel de granularidad: funciones atomicas vs composiciones
2. **Si la peticion es ambigua**, preguntar al usuario SOLO lo esencial (no mas de 2 preguntas).
---
## FASE 2: OBSERVAR — Consultar el registry
Consultar `registry.db` para encontrar funciones existentes relevantes y evitar duplicados.
```bash
# Buscar funciones similares por nombre y descripcion (OBLIGATORIO — usar multiples terminos)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
# Buscar tipos relacionados
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Funciones del dominio objetivo
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
# Tipos del dominio objetivo
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
# Funciones que podrian componerse (misma firma de retorno)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
```
**Clasificar resultados en:**
- **Reutilizables directamente**: funciones que ya hacen lo que se necesita
- **Componibles**: funciones que pueden usarse como building blocks
- **Similares pero diferentes**: funciones parecidas que confirman que no hay duplicado exacto
---
## FASE 3: PLANIFICAR — Disenar las funciones con un agente Plan
Invocar el Agent tool con `subagent_type: "Plan"` para disenar la lista de funciones a crear.
El prompt al agente Plan debe incluir:
- La peticion original del usuario
- Las funciones existentes encontradas en FASE 2 (IDs y descripciones)
- Los tipos existentes relevantes
- Las reglas de pureza del registry
El agente Plan debe producir una lista estructurada de funciones a crear, cada una con:
- **nombre** (snake_case)
- **kind** (function | pipeline | component)
- **lang** (go | py | bash | typescript)
- **domain**
- **purity** (pure | impure) — justificando por que
- **signature** propuesta
- **description** breve
- **uses_functions** — IDs de funciones existentes que reutiliza
- **uses_types** — IDs de tipos existentes que usa
- **dependencias** — si una funcion nueva depende de otra funcion nueva del mismo batch, indicar el orden
- **tests** — que se debe testear (casos de exito, edge cases, errores)
**Reglas del plan:**
- Funciones puras primero, impuras despues, pipelines al final
- Maximizar reutilizacion de funciones existentes
- Cada funcion debe tener tests propuestos
- El plan debe indicar el **orden de creacion** (las que tienen dependencias internas van despues)
- Agrupar funciones independientes para creacion en paralelo
**NO pedir confirmacion al usuario** — proceder directamente a la fase de construccion. Mostrar el plan brevemente en el output como referencia pero sin pausar:
---
## FASE 4: CONSTRUIR — Crear funciones en paralelo con fn-constructor
Para cada batch del plan, lanzar agentes `fn-constructor` **en paralelo** (un agente por funcion o grupo pequeno de funciones relacionadas).
**Como invocar cada fn-constructor:**
Usar el Agent tool con `subagent_type: "fn-constructor"` pasando un prompt completo con:
```
Crea la siguiente funcion para el registry fn_registry en /home/lucas/fn_registry:
Funcion: {nombre}
Kind: {kind}
Lang: {lang}
Domain: {domain}
Purity: {purity}
Signature: {signature}
Description: {descripcion}
Uses_functions: [{ids}]
Uses_types: [{ids}]
Tests requeridos:
- {test1}: {descripcion del test}
- {test2}: {descripcion del test}
- {test3}: {descripcion del test}
Contexto: Esta funcion es parte de un batch para {descripcion general del objetivo}.
Funciones existentes del registry que puedes reutilizar: {ids relevantes}
IMPORTANTE:
- Crear el archivo de codigo Y el .md con frontmatter completo
- Crear el archivo de tests correspondiente
- Marcar tested: true en el .md si creas tests
- Respetar las reglas de pureza
- Usar tipos nativos en la firma
- file_path relativo a la raiz del registry
- NO ejecutar fn index (lo hare yo al final)
```
**Orden de ejecucion:**
1. Lanzar todos los fn-constructor del Batch 1 en paralelo
2. Esperar a que terminen
3. Lanzar todos los fn-constructor del Batch 2 en paralelo (dependen de Batch 1)
4. Repetir para cada batch subsiguiente
**Sin limite de agentes en paralelo** — lanzar todos los fn-constructor del batch simultaneamente para maxima velocidad.
---
## FASE 5: INDEXAR — Registrar todo en el registry
Despues de que TODOS los fn-constructor terminen:
```bash
# Indexar todo de una vez
cd /home/lucas/fn_registry && ./fn index
```
Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
- ID duplicado → renombrar
- uses_functions referencia ID inexistente → verificar que el batch anterior se creo correctamente
- Violacion de pureza → ajustar purity o quitar dependencia impura
- file_path incorrecto → corregir la ruta
---
## FASE 6: VERIFICAR — Asegurar que todo esta correcto
### 6.1 Verificar indexacion
```bash
# Verificar cada funcion creada
cd /home/lucas/fn_registry
./fn show {id_de_cada_funcion}
# Verificar que no hay funciones sin params_schema
./fn check params
```
### 6.2 Ejecutar tests
Para cada funcion con tests, ejecutar:
```bash
cd /home/lucas/fn_registry
# Go
CGO_ENABLED=1 go test -tags fts5 -v -run TestNombreDelTest ./functions/{domain}/
# Python
python/.venv/bin/python3 -m pytest python/functions/{domain}/{nombre}_test.py -v
# TypeScript
cd frontend && pnpm exec vitest run functions/{domain}/{nombre}.test.ts
# Bash (si hay tests)
bash bash/functions/{domain}/{nombre}_test.sh
```
### 6.3 Verificar integridad
```bash
# Verificar que todas las funciones nuevas estan en la BD
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
# Verificar que los tests estan indexados
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
# Verificar dependencias
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
```
### 6.4 Si algo fallo
- Si un test falla → corregir el codigo y re-ejecutar
- Si una funcion no se indexo → verificar el .md y re-indexar
- Si hay errores de integridad → corregir y re-indexar
- NO continuar al reporte si hay tests fallando o funciones sin indexar
---
## FASE 7: REPORTE — Resumen final
```
=== FUNCIONES CREADAS ===
Peticion: {descripcion original}
Funciones del registry reutilizadas:
- {id}: {descripcion}
Funciones nuevas:
- {id} [{kind}, {purity}, {lang}] — {descripcion}
Tests: N pasando
Archivo: {file_path}
- {id} [{kind}, {purity}, {lang}] — {descripcion}
Tests: N pasando
Archivo: {file_path}
Tipos nuevos:
- {id}: {descripcion}
Tests: X/Y pasando
Indexacion: OK
Para usar estas funciones:
# Go
import "fn_registry/functions/{domain}"
result := domain.FunctionName(args)
# Python
from {domain} import function_name
# Bash
source "$FN_REGISTRY_ROOT/bash/functions/{domain}/{name}.sh"
```
---
## Reglas
- **SIEMPRE** consultar registry.db antes de crear — evitar duplicados
- **NO pedir confirmacion** — mostrar el plan brevemente y proceder directamente
- **SIEMPRE** crear tests para cada funcion
- **SIEMPRE** indexar y verificar despues de crear
- **Funciones puras primero**, impuras despues, pipelines al final
- **Maximizar paralelismo** en la creacion (agentes fn-constructor en paralelo)
- **Maximizar reutilizacion** de funciones existentes
- **NO crear funciones especificas de una app** — solo codigo reutilizable y generico
- Si el usuario pide algo que ya existe, informar y sugerir reutilizar en vez de duplicar
- Si una funcion del batch falla, las demas del mismo batch pueden continuar independientemente
$ARGUMENTS
+60
View File
@@ -0,0 +1,60 @@
#include "dashboard_grid.h"
#include <imgui.h>
#include <vector>
// Internal state stack to support nested grids.
namespace {
struct GridState {
int columns;
float spacing;
float col_width;
int counter; // number of dashboard_grid_next() calls so far
};
static std::vector<GridState> g_grid_stack;
} // namespace
void dashboard_grid_begin(int columns, float spacing) {
if (columns < 1) columns = 1;
float available = ImGui::GetContentRegionAvail().x;
float col_width = (available - spacing * static_cast<float>(columns - 1))
/ static_cast<float>(columns);
if (col_width < 1.0f) col_width = 1.0f;
g_grid_stack.push_back({columns, spacing, col_width, 0});
ImGui::BeginGroup();
ImGui::PushItemWidth(col_width);
}
void dashboard_grid_next() {
if (g_grid_stack.empty()) return;
GridState& s = g_grid_stack.back();
ImGui::PopItemWidth();
ImGui::EndGroup();
s.counter++;
if (s.counter % s.columns != 0) {
// Same row: advance horizontally.
ImGui::SameLine(0.0f, s.spacing);
}
// If counter % columns == 0 the next BeginGroup starts a new row automatically.
ImGui::BeginGroup();
ImGui::PushItemWidth(s.col_width);
}
void dashboard_grid_end() {
if (g_grid_stack.empty()) return;
ImGui::PopItemWidth();
ImGui::EndGroup();
g_grid_stack.pop_back();
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
// Dashboard grid — distributes child widgets in N columns.
// Usage:
// dashboard_grid_begin(3);
// // widget 1 (auto placed in col 0)
// dashboard_grid_next();
// // widget 2 (auto placed in col 1)
// dashboard_grid_next();
// // widget 3 (auto placed in col 2, wraps to next row)
// dashboard_grid_next();
// // widget 4 (col 0 of row 2)
// dashboard_grid_end();
void dashboard_grid_begin(int columns = 2, float spacing = 8.0f);
void dashboard_grid_next(); // advance to next cell
void dashboard_grid_end();
+88
View File
@@ -0,0 +1,88 @@
---
name: dashboard_grid
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void dashboard_grid_begin(int columns = 2, float spacing = 8.0f); void dashboard_grid_next(); void dashboard_grid_end()"
description: "Grid de N columnas para distribuir widgets de dashboard automaticamente con spacing uniforme entre columnas"
tags: [imgui, grid, layout, dashboard, responsive]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/dashboard_grid.cpp"
framework: imgui
params:
- name: columns
desc: "Numero de columnas del grid (minimo 1); el ancho disponible se divide uniformemente entre ellas"
- name: spacing
desc: "Espacio horizontal entre columnas en pixels"
output: "Layout de grid aplicado al contenido entre dashboard_grid_begin/end; cada celda recibe un ancho uniforme calculado a partir del espacio disponible"
---
# dashboard_grid
Divide el ancho disponible en N columnas con spacing uniforme y posiciona cada widget en su celda automaticamente. Soporta grids anidados mediante un stack interno de estado.
## Uso
```cpp
dashboard_grid_begin(3, 8.0f);
// Celda 0
dashboard_panel_begin("CPU");
ImGui::Text("%.1f %%", cpu_pct);
dashboard_panel_end();
dashboard_grid_next();
// Celda 1
dashboard_panel_begin("Memory");
ImGui::Text("%.0f MB", mem_mb);
dashboard_panel_end();
dashboard_grid_next();
// Celda 2
dashboard_panel_begin("Disk");
ImGui::Text("%.0f GB", disk_gb);
dashboard_panel_end();
dashboard_grid_end();
```
## Implementacion
### Calculo de ancho
```
col_width = (available_width - spacing * (columns - 1)) / columns
```
`available_width` se obtiene de `ImGui::GetContentRegionAvail().x` en el momento de `dashboard_grid_begin`.
### Mecanica de celdas
Cada celda es un `BeginGroup`/`EndGroup` con `PushItemWidth(col_width)` para que los widgets internos respeten el ancho de columna. Al llamar `dashboard_grid_next`:
1. Se cierra la celda actual (`PopItemWidth`, `EndGroup`).
2. Se incrementa el contador interno.
3. Si `counter % columns != 0` se emite `SameLine(0, spacing)` para continuar en la misma fila.
4. Si `counter % columns == 0` no se emite `SameLine`: ImGui pasa a la siguiente fila automaticamente.
5. Se abre la nueva celda (`BeginGroup`, `PushItemWidth`).
### Grids anidados
El estado (columnas, spacing, col_width, counter) se guarda en `g_grid_stack` (un `std::vector` de structs en un namespace anonimo). Cada llamada a `dashboard_grid_begin` hace `push_back` y `dashboard_grid_end` hace `pop_back`, permitiendo anidar grids sin conflicto.
## Notas
- Llamar `dashboard_grid_next()` entre cada par de widgets, **no** antes del primero ni despues del ultimo.
- El numero de `dashboard_grid_next()` puede ser mayor que `columns - 1`: el grid hace wrap automatico a la siguiente fila.
- Combina bien con `dashboard_panel_begin`/`dashboard_panel_end` para crear dashboards con paneles alineados en cuadricula.
- Si `columns <= 0` se fuerza a 1 para evitar division por cero.
+24
View File
@@ -0,0 +1,24 @@
#include "dashboard_panel.h"
#include <imgui.h>
bool dashboard_panel_begin(const char* title, float min_width, float min_height) {
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.15f, 1.0f));
ImVec2 size(min_width > 0.0f ? min_width : 0.0f,
min_height > 0.0f ? min_height : 0.0f);
ImGui::BeginChild(title, size, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY);
ImGui::TextUnformatted(title);
ImGui::Separator();
return true;
}
void dashboard_panel_end() {
ImGui::EndChild();
ImGui::PopStyleColor(1);
ImGui::PopStyleVar(2);
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
// Dashboard panel — a styled child window with title bar.
// Usage:
// if (dashboard_panel_begin("Sales")) {
// line_plot("Revenue", xs, ys, N);
// }
// dashboard_panel_end(); // ALWAYS call, even if begin returned false
//
// Features: title bar with text, rounded corners, subtle border, auto-resize.
// min_width/min_height set minimum size constraints.
bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f);
void dashboard_panel_end();
+57
View File
@@ -0,0 +1,57 @@
---
name: dashboard_panel
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f); void dashboard_panel_end()"
description: "Contenedor estilizado tipo panel para dashboards con titulo, bordes redondeados y tamaño minimo configurable"
tags: [imgui, panel, container, layout, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/dashboard_panel.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del panel, tambien sirve como ID de ImGui para distinguir multiples paneles"
- name: min_width
desc: "Ancho minimo del panel en pixels (0 = sin restriccion)"
- name: min_height
desc: "Alto minimo del panel en pixels (0 = sin restriccion)"
output: "true si el panel es visible y se debe renderizar contenido; llamar siempre dashboard_panel_end() independientemente del valor de retorno"
---
# dashboard_panel
Panel estilizado para dashboards ImGui. Envuelve un `BeginChild`/`EndChild` con estilos predefinidos: fondo oscuro (`#1F1F26`), bordes redondeados (5 px), borde visible y separador bajo el titulo.
## Uso
```cpp
if (dashboard_panel_begin("Revenue", 300.0f, 200.0f)) {
line_plot("Revenue", xs, ys, N);
}
dashboard_panel_end(); // siempre llamar
```
## Implementacion
- `PushStyleVar` aplica `ChildRounding = 5.0f` y `ChildBorderSize = 1.0f`
- `PushStyleColor` establece el fondo del child a `(0.12, 0.12, 0.15, 1.0)`
- `BeginChild` con `ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY`
- Titulo con `TextUnformatted` seguido de `Separator`
- `dashboard_panel_end` hace `EndChild`, `PopStyleColor(1)`, `PopStyleVar(2)`
## Notas
- El titulo actua como ID de ImGui: dos paneles con el mismo titulo en el mismo frame se comportan como uno solo. Usar `##` para diferenciar IDs si se repite el texto: `"Revenue##panel1"`.
- `AutoResizeY` hace que el panel crezca verticalmente con su contenido; `min_height` establece el piso.
- El patron begin/end es idomatico en ImGui: `end` debe llamarse siempre para hacer pop de los estilos, aunque `begin` retorne false.
+53
View File
@@ -0,0 +1,53 @@
#include "core/docking_layout.h"
#include "imgui_internal.h"
ImGuiID docking_layout(DockPreset preset) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGuiID dockspace_id = ImGui::DockSpaceOverViewport(0, viewport);
static bool initialized = false;
if (initialized) {
return dockspace_id;
}
initialized = true;
if (preset == DockPreset::Default) {
return dockspace_id;
}
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
if (preset == DockPreset::TwoColumns) {
ImGuiID right;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.6f, nullptr, &right);
} else if (preset == DockPreset::ThreeColumns) {
ImGuiID center, right;
ImGuiID left_node;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.33f, &left_node, &center);
ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.5f, nullptr, &right);
} else if (preset == DockPreset::SidebarLeft) {
ImGuiID main;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.25f, nullptr, &main);
} else if (preset == DockPreset::SidebarRight) {
ImGuiID sidebar;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.25f, &sidebar, nullptr);
} else if (preset == DockPreset::TopBottom) {
ImGuiID bottom;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.6f, nullptr, &bottom);
} else if (preset == DockPreset::Dashboard) {
ImGuiID top, bottom;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.5f, &top, &bottom);
ImGui::DockBuilderSplitNode(top, ImGuiDir_Left, 0.5f, nullptr, nullptr);
ImGui::DockBuilderSplitNode(bottom, ImGuiDir_Left, 0.5f, nullptr, nullptr);
}
ImGui::DockBuilderFinish(dockspace_id);
return dockspace_id;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "imgui.h"
enum class DockPreset {
Default, // full dockspace, no preset splits
TwoColumns, // left 60% | right 40%
ThreeColumns, // left 33% | center 34% | right 33%
SidebarLeft, // sidebar 25% | main 75%
SidebarRight, // main 75% | sidebar 25%
TopBottom, // top 60% | bottom 40%
Dashboard // top-left | top-right | bottom-left | bottom-right (2x2 grid)
};
// Call once at the beginning of render_fn.
// Returns the dockspace ID (use with ImGui::SetNextWindowDockID if needed).
ImGuiID docking_layout(DockPreset preset = DockPreset::Default);
+64
View File
@@ -0,0 +1,64 @@
---
name: docking_layout
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "ImGuiID docking_layout(DockPreset preset = DockPreset::Default)"
description: "Configura un docking space con presets de layout predefinidos para dashboards"
tags: [imgui, docking, layout, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/docking_layout.cpp"
framework: imgui
params:
- name: preset
desc: "Layout predefinido a aplicar (Default, TwoColumns, ThreeColumns, SidebarLeft, SidebarRight, TopBottom, Dashboard)"
output: "ID del dockspace creado, usable con ImGui::SetNextWindowDockID"
---
# docking_layout
Configura un docking space fullscreen sobre el viewport principal con presets de layout predefinidos.
Usa `DockSpaceOverViewport` para crear el dockspace y, en el primer frame, aplica el preset con `DockBuilderSplitNode`. Llamar una vez al inicio de cada frame, antes de renderizar las ventanas hijas.
## Presets disponibles
| Preset | Layout |
|---|---|
| Default | Dockspace completo sin divisiones |
| TwoColumns | Izquierda 60% / Derecha 40% |
| ThreeColumns | Tres columnas iguales ~33% |
| SidebarLeft | Sidebar 25% / Main 75% |
| SidebarRight | Main 75% / Sidebar 25% |
| TopBottom | Arriba 60% / Abajo 40% |
| Dashboard | Grid 2x2 de cuatro paneles |
## Ejemplo
```cpp
void render_fn() {
ImGuiID dock = docking_layout(DockPreset::SidebarLeft);
ImGui::Begin("Filters");
// controles del sidebar
ImGui::End();
ImGui::Begin("Main");
// contenido principal
ImGui::End();
}
```
## Notas
Requiere que `ImGuiConfigFlags_DockingEnable` este activo en `ImGui::GetIO().ConfigFlags` (habilitado por `app_base.cpp`). El preset se aplica solo en el primer frame (static bool). `imgui_internal.h` es necesario para `DockBuilder*`.
+174
View File
@@ -0,0 +1,174 @@
#include "graph_spatial_hash.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static inline int floor_div(float v, float cell) {
return static_cast<int>(std::floor(v / cell));
}
static inline float sq(float x) { return x * x; }
// ---------------------------------------------------------------------------
// Snapshot de posiciones (almacenado internamente en build)
// La interfaz del header no pasa xs/ys a los metodos de query, por lo que
// SpatialHash conserva copias internas para calcular distancias.
// ---------------------------------------------------------------------------
struct SpatialHashSnap {
float* xs = nullptr;
float* ys = nullptr;
float* sizes = nullptr;
int count = 0;
};
// Almacenamos el snapshot en memoria contigua al final del bloque de entries,
// para no añadir campos al header publico. Usamos un puntero opaco en un
// campo reservado. Como el header ya esta fijado, guardamos el snapshot como
// una variable estatica por instancia — aceptable para uso single-threaded
// tipico de ImGui. Para multi-instancia se puede usar un map.
//
// En la practica, la convencion del registry es que SpatialHash se crea una
// vez por frame de ImGui y se usa en el mismo hilo de render.
static SpatialHashSnap g_snap; // snapshot mas reciente
// ---------------------------------------------------------------------------
// SpatialHash
// ---------------------------------------------------------------------------
int SpatialHash::cell_hash(int cx, int cy) const {
unsigned int h = static_cast<unsigned int>(cx) * 73856093u
^ static_cast<unsigned int>(cy) * 19349663u;
return static_cast<int>(h % static_cast<unsigned int>(table_size));
}
SpatialHash::SpatialHash(float cell_size_, int table_size_)
: cell_size(cell_size_)
, table_size(table_size_)
, entry_count(0)
, entry_capacity(256)
{
buckets = static_cast<int*>(std::malloc(
static_cast<std::size_t>(table_size) * sizeof(int)));
entries = static_cast<int*>(std::malloc(
static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));
std::memset(buckets, -1,
static_cast<std::size_t>(table_size) * sizeof(int));
}
SpatialHash::~SpatialHash() {
std::free(buckets);
std::free(entries);
std::free(g_snap.xs);
std::free(g_snap.ys);
std::free(g_snap.sizes);
g_snap = {};
}
void SpatialHash::build(const float* xs, const float* ys, const float* sizes, int count) {
// --- Limpiar tabla ---
std::memset(buckets, -1,
static_cast<std::size_t>(table_size) * sizeof(int));
entry_count = 0;
// --- Snapshot de posiciones para queries ---
if (g_snap.count < count) {
std::free(g_snap.xs);
std::free(g_snap.ys);
std::free(g_snap.sizes);
g_snap.xs = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
g_snap.ys = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
g_snap.sizes = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
}
std::memcpy(g_snap.xs, xs, static_cast<std::size_t>(count) * sizeof(float));
std::memcpy(g_snap.ys, ys, static_cast<std::size_t>(count) * sizeof(float));
std::memcpy(g_snap.sizes, sizes, static_cast<std::size_t>(count) * sizeof(float));
g_snap.count = count;
// --- Insertar nodos en la tabla hash ---
for (int i = 0; i < count; ++i) {
int cx = floor_div(xs[i], cell_size);
int cy = floor_div(ys[i], cell_size);
int bucket = cell_hash(cx, cy);
// Crecer entries si necesario
if (entry_count >= entry_capacity) {
entry_capacity *= 2;
entries = static_cast<int*>(std::realloc(
entries,
static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));
}
// Insertar al frente de la cadena encadenada
int slot = entry_count++;
entries[slot * 2 + 0] = i; // node_index
entries[slot * 2 + 1] = buckets[bucket]; // next_in_chain
buckets[bucket] = slot;
}
}
int SpatialHash::query_nearest(float qx, float qy, float radius, float* out_dist) const {
int best_idx = -1;
float best_d = radius; // umbral: solo aceptamos dentro del radio
int qcx = floor_div(qx, cell_size);
int qcy = floor_div(qy, cell_size);
// Escanear vecindad 3x3 de celdas
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
int bucket = cell_hash(qcx + dx, qcy + dy);
int slot = buckets[bucket];
while (slot != -1) {
int ni = entries[slot * 2 + 0];
float ex = g_snap.xs[ni] - qx;
float ey = g_snap.ys[ni] - qy;
float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
if (dist < best_d) {
best_d = dist;
best_idx = ni;
}
slot = entries[slot * 2 + 1];
}
}
}
if (out_dist && best_idx != -1)
*out_dist = best_d;
return best_idx;
}
int SpatialHash::query_radius(float qx, float qy, float radius, int* out, int max_out) const {
int found = 0;
int qcx = floor_div(qx, cell_size);
int qcy = floor_div(qy, cell_size);
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
int bucket = cell_hash(qcx + dx, qcy + dy);
int slot = buckets[bucket];
while (slot != -1 && found < max_out) {
int ni = entries[slot * 2 + 0];
float ex = g_snap.xs[ni] - qx;
float ey = g_snap.ys[ni] - qy;
float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
if (dist <= radius)
out[found++] = ni;
slot = entries[slot * 2 + 1];
}
}
}
return found;
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
// SpatialHash — grid espacial para hit-testing de nodos en grafos.
// Permite buscar el nodo mas cercano a una posicion en O(1) amortizado.
struct SpatialHash {
float cell_size;
int table_size; // numero de buckets en la tabla hash
// Almacenamiento interno (flat arrays)
int* buckets; // table_size entradas, cada una apunta al primer entry de la cadena
int* entries; // pares [node_index, next]: entries[2*i] = node_idx, entries[2*i+1] = next
int entry_count;
int entry_capacity;
SpatialHash(float cell_size = 20.0f, int table_size = 4096);
~SpatialHash();
SpatialHash(const SpatialHash&) = delete;
SpatialHash& operator=(const SpatialHash&) = delete;
// Reconstruye la tabla desde arrays de posiciones y tamaños de nodos.
// xs[i], ys[i]: posicion del nodo i. sizes[i]: radio del nodo i.
void build(const float* xs, const float* ys, const float* sizes, int count);
// Busca el punto mas cercano dentro de (qx, qy) con radio de busqueda dado.
// Descuenta el tamaño del nodo (circulo): distancia efectiva = dist(centro) - size.
// Retorna el indice del nodo, o -1 si no hay ninguno.
// Si out_dist es no-nulo, escribe la distancia al nodo encontrado.
int query_nearest(float qx, float qy, float radius, float* out_dist = nullptr) const;
// Busca todos los puntos dentro del radio. Escribe indices en out[].
// Retorna el numero de nodos encontrados (hasta max_out).
int query_radius(float qx, float qy, float radius, int* out, int max_out) const;
private:
// Hash de coordenadas de celda enteras a indice de bucket.
int cell_hash(int cx, int cy) const;
};
+71
View File
@@ -0,0 +1,71 @@
---
name: graph_spatial_hash
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "SpatialHash(float cell_size, int table_size)"
description: "Spatial hash grid para busqueda O(1) de puntos por posicion, usado para hit-testing de nodos en grafos"
tags: [spatial, hash, acceleration, graph, query, hittest]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/graph_spatial_hash.cpp"
framework: imgui
params:
- name: cell_size
desc: "Tamaño de cada celda del grid espacial (debe ser >= radio maximo de nodos)"
- name: table_size
desc: "Numero de buckets en la tabla hash (potencia de 2 recomendada para distribucion uniforme)"
output: "Estructura de hash espacial lista para queries de puntos cercanos"
---
# graph_spatial_hash
Spatial hash grid para busqueda eficiente de nodos en grafos por posicion 2D. Util para hit-testing de nodos en editores de grafos basados en ImGui, donde se necesita saber que nodo esta bajo el cursor en cada frame.
## Uso
```cpp
SpatialHash sh(20.0f, 4096);
// Reconstruir cada frame (o cuando cambian posiciones)
sh.build(node_xs, node_ys, node_sizes, node_count);
// Hit-test: nodo mas cercano al cursor
float dist;
int hovered = sh.query_nearest(mouse_x, mouse_y, 30.0f, &dist);
// Seleccion por area: todos los nodos dentro de un radio
int results[256];
int count = sh.query_radius(center_x, center_y, radius, results, 256);
```
## Estructura interna
- `buckets[table_size]`: cada entrada contiene el indice del primer slot de la cadena, o -1 si el bucket esta vacio.
- `entries[2 * entry_capacity]`: pares `[node_index, next_in_chain]` almacenados como flat array. Se expande con `realloc` si el numero de nodos supera la capacidad inicial (256).
- El snapshot de posiciones (`xs`, `ys`, `sizes`) se copia internamente en `build()` para que `query_nearest` y `query_radius` puedan calcular distancias sin recibir los arrays como parametro.
## Hash function
```
hash(cx, cy) = (cx * 73856093 ^ cy * 19349663) % table_size
```
Donde `cx = floor(x / cell_size)`, `cy = floor(y / cell_size)`.
## Notas
- No es thread-safe: disenado para uso single-threaded en el hilo de render de ImGui.
- `build()` debe llamarse cada frame si las posiciones de los nodos cambian.
- `cell_size` debe ser >= al radio maximo de los nodos para que la vecindad 3x3 capture todos los candidatos posibles.
- La distancia efectiva al nodo se calcula como `dist(centro) - size`, de modo que el hit-test funciona correctamente para nodos de distintos tamaños.
- No implementa `operator=` ni copy constructor (deleted) para evitar doble-free de los arrays internos.
+99
View File
@@ -0,0 +1,99 @@
#include "core/memory_overlay.h"
#include "imgui.h"
#ifdef TRACY_ENABLE
#include "tracy/Tracy.hpp"
#endif
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#pragma comment(lib, "psapi.lib")
#else
#include <cstdio>
#endif
#include <cstring>
namespace {
struct MemStats {
long rss_kb = -1; // Resident Set Size
long peak_kb = -1; // Peak RSS
long vsize_kb = -1; // Virtual memory size
};
static MemStats s_cached;
static double s_last_sample = -1.0;
#ifdef _WIN32
static MemStats sample_memory() {
MemStats s;
PROCESS_MEMORY_COUNTERS pmc{};
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
s.rss_kb = static_cast<long>(pmc.WorkingSetSize / 1024);
s.peak_kb = static_cast<long>(pmc.PeakWorkingSetSize / 1024);
s.vsize_kb = -1; // not easily available on Windows without VirtualQuery loop
}
return s;
}
#else
static MemStats sample_memory() {
MemStats s;
FILE* f = std::fopen("/proc/self/status", "r");
if (!f) return s;
char line[128];
while (std::fgets(line, sizeof(line), f)) {
long val = 0;
if (std::sscanf(line, "VmRSS: %ld kB", &val) == 1) s.rss_kb = val;
if (std::sscanf(line, "VmPeak: %ld kB", &val) == 1) s.peak_kb = val;
if (std::sscanf(line, "VmSize: %ld kB", &val) == 1) s.vsize_kb = val;
}
std::fclose(f);
return s;
}
#endif
} // namespace
void memory_overlay() {
#ifdef TRACY_ENABLE
ZoneScoped;
#endif
// Sample at most once per second
const double now = ImGui::GetTime();
if (s_last_sample < 0.0 || (now - s_last_sample) >= 1.0) {
s_cached = sample_memory();
s_last_sample = now;
}
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_AlwaysAutoResize
| ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_NoFocusOnAppearing
| ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoMove;
const float pad = 10.0f;
const ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 pos(vp->WorkPos.x + vp->WorkSize.x - pad,
vp->WorkPos.y + vp->WorkSize.y - pad);
ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.65f);
if (ImGui::Begin("##memory_overlay", nullptr, flags)) {
if (s_cached.rss_kb >= 0) {
ImGui::Text("RSS: %5ld MB", s_cached.rss_kb / 1024);
ImGui::Text("Peak: %5ld MB", s_cached.peak_kb / 1024);
#ifndef _WIN32
ImGui::Text("VSize: %5ld MB", s_cached.vsize_kb / 1024);
#endif
} else {
ImGui::TextDisabled("Memory: N/A");
}
}
ImGui::End();
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
// Renders a memory statistics overlay in the bottom-right corner.
// Call within an ImGui frame once per frame.
// Samples /proc/self/status (Linux) or GetProcessMemoryInfo (Windows) at most
// once per second; results are cached between samples.
void memory_overlay();
+56
View File
@@ -0,0 +1,56 @@
---
name: memory_overlay
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void memory_overlay()"
description: "Renderiza un overlay de estadisticas de memoria (RSS, peak, vsize) en la esquina inferior derecha"
tags: [imgui, memory, overlay, debug, dashboard, profiling]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/memory_overlay.cpp"
framework: imgui
params: []
output: "Renderiza el overlay de memoria en el frame ImGui actual"
---
# memory_overlay
Muestra estadísticas de memoria del proceso en una ventana semi-transparente en la esquina inferior derecha. Complementa `fps_overlay` para un dashboard mínimo de rendimiento.
Las métricas se leen como máximo una vez por segundo y se cachean en una variable estática local, evitando I/O excesivo en el hot path de render.
## Métricas mostradas
| Campo | Linux | Windows |
|---|---|---|
| RSS (MB) | `VmRSS` de `/proc/self/status` | `WorkingSetSize` (PSAPI) |
| Peak RSS (MB) | `VmPeak` de `/proc/self/status` | `PeakWorkingSetSize` (PSAPI) |
| Virtual Size (MB) | `VmSize` de `/proc/self/status` | N/A (no mostrado) |
En Windows se requiere enlazar `psapi.lib` (el `.cpp` incluye `#pragma comment(lib, "psapi.lib")`).
## Ejemplo
```cpp
// En el loop de render, dentro de un frame ImGui:
fps_overlay();
memory_overlay();
```
## Notas
- La función sigue la misma convención que `fps_overlay`: mismos flags de ventana, mismo alpha (0.65).
- Posición: esquina inferior derecha (`pivot = {1,1}`), separada 10 px del borde.
- La lectura de `/proc/self/status` es I/O local de ~0.1 ms, amortizada a 1 Hz — despreciable.
- Con `TRACY_ENABLE` activo, la función registra una zona `ZoneScoped` para que aparezca en el profiler.
- Si la plataforma no puede leer las estadísticas, muestra "Memory: N/A" en gris.
+131
View File
@@ -0,0 +1,131 @@
#include "plot_theme.h"
// ---------------------------------------------------------------------------
// Preset structs
// ---------------------------------------------------------------------------
PlotTheme plot_theme_preset_dark() {
PlotTheme t;
t.name = "dark";
t.bg = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
t.frame_bg = ImVec4(0.14f, 0.14f, 0.17f, 1.00f);
t.text = ImVec4(0.88f, 0.88f, 0.90f, 1.00f);
t.grid = ImVec4(0.30f, 0.30f, 0.35f, 0.60f);
t.palette_count = 10;
t.palette[0] = ImVec4(0.33f, 0.62f, 0.91f, 1.00f); // steel blue
t.palette[1] = ImVec4(0.35f, 0.78f, 0.54f, 1.00f); // soft green
t.palette[2] = ImVec4(0.96f, 0.60f, 0.25f, 1.00f); // warm orange
t.palette[3] = ImVec4(0.85f, 0.38f, 0.42f, 1.00f); // muted red
t.palette[4] = ImVec4(0.72f, 0.52f, 0.88f, 1.00f); // lavender
t.palette[5] = ImVec4(0.38f, 0.80f, 0.82f, 1.00f); // teal
t.palette[6] = ImVec4(0.95f, 0.80f, 0.32f, 1.00f); // gold
t.palette[7] = ImVec4(0.60f, 0.82f, 0.38f, 1.00f); // lime
t.palette[8] = ImVec4(0.90f, 0.55f, 0.75f, 1.00f); // pink
t.palette[9] = ImVec4(0.55f, 0.70f, 0.95f, 1.00f); // periwinkle
return t;
}
PlotTheme plot_theme_preset_light() {
PlotTheme t;
t.name = "light";
t.bg = ImVec4(0.97f, 0.97f, 0.97f, 1.00f);
t.frame_bg = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
t.text = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
t.grid = ImVec4(0.70f, 0.70f, 0.72f, 0.60f);
t.palette_count = 10;
t.palette[0] = ImVec4(0.12f, 0.47f, 0.71f, 1.00f); // deep blue
t.palette[1] = ImVec4(0.17f, 0.63f, 0.17f, 1.00f); // deep green
t.palette[2] = ImVec4(0.84f, 0.37f, 0.00f, 1.00f); // burnt orange
t.palette[3] = ImVec4(0.84f, 0.15f, 0.16f, 1.00f); // vivid red
t.palette[4] = ImVec4(0.58f, 0.40f, 0.74f, 1.00f); // purple
t.palette[5] = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // cyan
t.palette[6] = ImVec4(0.74f, 0.74f, 0.13f, 1.00f); // olive
t.palette[7] = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // sky blue
t.palette[8] = ImVec4(0.89f, 0.47f, 0.76f, 1.00f); // rose
t.palette[9] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); // grey
return t;
}
PlotTheme plot_theme_preset_high_contrast() {
PlotTheme t;
t.name = "high_contrast";
t.bg = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
t.frame_bg = ImVec4(0.05f, 0.05f, 0.05f, 1.00f);
t.text = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
t.grid = ImVec4(0.30f, 0.30f, 0.30f, 0.80f);
t.palette_count = 10;
t.palette[0] = ImVec4(0.00f, 1.00f, 1.00f, 1.00f); // cyan neon
t.palette[1] = ImVec4(0.00f, 1.00f, 0.00f, 1.00f); // green neon
t.palette[2] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); // orange neon
t.palette[3] = ImVec4(1.00f, 0.00f, 0.50f, 1.00f); // magenta
t.palette[4] = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); // yellow
t.palette[5] = ImVec4(0.40f, 0.80f, 1.00f, 1.00f); // sky blue
t.palette[6] = ImVec4(1.00f, 0.40f, 0.40f, 1.00f); // salmon
t.palette[7] = ImVec4(0.60f, 1.00f, 0.40f, 1.00f); // lime
t.palette[8] = ImVec4(1.00f, 0.80f, 1.00f, 1.00f); // lavender bright
t.palette[9] = ImVec4(0.80f, 1.00f, 0.80f, 1.00f); // mint bright
return t;
}
// ---------------------------------------------------------------------------
// Internal helper
// ---------------------------------------------------------------------------
static void apply_imgui_colors(const PlotTheme& theme) {
ImGuiStyle& s = ImGui::GetStyle();
s.Colors[ImGuiCol_WindowBg] = theme.bg;
s.Colors[ImGuiCol_ChildBg] = theme.bg;
s.Colors[ImGuiCol_PopupBg] = theme.frame_bg;
s.Colors[ImGuiCol_FrameBg] = theme.frame_bg;
s.Colors[ImGuiCol_FrameBgHovered] = theme.frame_bg;
s.Colors[ImGuiCol_FrameBgActive] = theme.frame_bg;
s.Colors[ImGuiCol_Text] = theme.text;
s.Colors[ImGuiCol_TextDisabled] = ImVec4(theme.text.x * 0.5f,
theme.text.y * 0.5f,
theme.text.z * 0.5f,
0.80f);
}
static void apply_implot_colors(const PlotTheme& theme) {
ImPlotStyle& ps = ImPlot::GetStyle();
ps.Colors[ImPlotCol_PlotBg] = theme.bg;
ps.Colors[ImPlotCol_PlotBorder] = theme.frame_bg;
ps.Colors[ImPlotCol_FrameBg] = theme.frame_bg;
ps.Colors[ImPlotCol_LegendBg] = theme.frame_bg;
ps.Colors[ImPlotCol_LegendBorder] = theme.grid;
ps.Colors[ImPlotCol_LegendText] = theme.text;
ps.Colors[ImPlotCol_TitleText] = theme.text;
ps.Colors[ImPlotCol_InlayText] = theme.text;
ps.Colors[ImPlotCol_AxisText] = theme.text;
ps.Colors[ImPlotCol_AxisGrid] = theme.grid;
ps.Colors[ImPlotCol_AxisTick] = theme.grid;
// Register custom colormap from palette
const int count = theme.palette_count > 10 ? 10 : theme.palette_count;
ImPlot::AddColormap(theme.name, theme.palette, count, false);
ImPlot::SetColormap(theme.name);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void plot_theme_apply(const PlotTheme& theme) {
apply_imgui_colors(theme);
apply_implot_colors(theme);
}
void plot_theme_dark() {
PlotTheme t = plot_theme_preset_dark();
plot_theme_apply(t);
}
void plot_theme_light() {
PlotTheme t = plot_theme_preset_light();
plot_theme_apply(t);
}
void plot_theme_high_contrast() {
PlotTheme t = plot_theme_preset_high_contrast();
plot_theme_apply(t);
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "imgui.h"
#include "implot.h"
struct PlotTheme {
const char* name;
ImVec4 bg; // plot background
ImVec4 frame_bg; // frame background
ImVec4 text; // text color
ImVec4 grid; // grid lines
ImVec4 palette[10]; // color palette for series
int palette_count;
};
// Preset themes
void plot_theme_dark();
void plot_theme_light();
void plot_theme_high_contrast();
// Custom theme
void plot_theme_apply(const PlotTheme& theme);
// Get preset theme structs (for inspection/modification)
PlotTheme plot_theme_preset_dark();
PlotTheme plot_theme_preset_light();
PlotTheme plot_theme_preset_high_contrast();
+96
View File
@@ -0,0 +1,96 @@
---
name: plot_theme
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void plot_theme_dark() / void plot_theme_light() / void plot_theme_high_contrast() / void plot_theme_apply(const PlotTheme& theme)"
description: "Gestiona temas y paletas de colores para ImPlot e ImGui, con presets dark/light/high-contrast y soporte para temas custom"
tags: [theme, colors, palette, styling, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui, implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/plot_theme.cpp"
framework: imgui
params:
- name: theme
desc: "Estructura PlotTheme con nombre, colores de fondo/frame/texto/grid y paleta de hasta 10 colores para series de datos"
output: "Aplica el tema al contexto ImPlot/ImGui actual modificando ImGuiStyle y ImPlotStyle en memoria"
---
# plot_theme
Configura el aspecto visual de ventanas ImGui y graficas ImPlot en un solo paso. Tres presets listos para usar y una API de tema custom para casos especificos.
## Presets
### `plot_theme_dark()`
Fondo oscuro (`#1A1A1F`) con paleta de 10 colores suaves: steel blue, soft green, warm orange, muted red, lavender, teal, gold, lime, pink, periwinkle. Ideal para dashboards de monitoreo.
### `plot_theme_light()`
Fondo claro (`#F7F7F7`) con colores vibrantes de alto contraste sobre blanco. Recomendado para capturas de pantalla o exportacion a documentos.
### `plot_theme_high_contrast()`
Fondo negro puro con colores neon: cyan, green, orange, magenta, yellow. Disenado para presentaciones en pantallas grandes o condiciones de poca luz.
## Uso
```cpp
#include "plot_theme.h"
// Al inicio del frame, antes de BeginMainMenuBar / Begin
plot_theme_dark();
// O con tema custom
PlotTheme my_theme = plot_theme_preset_dark();
my_theme.palette[0] = ImVec4(1.0f, 0.0f, 0.5f, 1.0f); // override primer color
plot_theme_apply(my_theme);
```
## API
```cpp
// Presets de aplicacion directa
void plot_theme_dark();
void plot_theme_light();
void plot_theme_high_contrast();
// Tema custom
void plot_theme_apply(const PlotTheme& theme);
// Obtener structs para inspeccion o modificacion parcial
PlotTheme plot_theme_preset_dark();
PlotTheme plot_theme_preset_light();
PlotTheme plot_theme_preset_high_contrast();
```
## Estructura PlotTheme
```cpp
struct PlotTheme {
const char* name; // nombre del colormap registrado en ImPlot
ImVec4 bg; // fondo del plot (ImPlotCol_PlotBg, ImGuiCol_WindowBg)
ImVec4 frame_bg; // fondo del frame (ImPlotCol_FrameBg, ImGuiCol_FrameBg)
ImVec4 text; // color del texto (todos los ImGui/ImPlot text cols)
ImVec4 grid; // lineas de cuadricula y ticks
ImVec4 palette[10]; // paleta de colores para series de datos
int palette_count; // numero de colores activos en la paleta (1-10)
};
```
## Notas
- `plot_theme_apply` llama a `ImPlot::AddColormap` y `ImPlot::SetColormap` con el campo `name` del tema como identificador. Si se llama dos veces con el mismo `name`, ImPlot registra el colormap de nuevo — usar nombres distintos si se crean multiples variantes en runtime.
- La funcion es "pure" en el sentido de que su unico efecto es escribir en `ImGuiStyle` e `ImPlotStyle` del contexto activo, sin I/O ni estado global propio.
- Requiere que `ImGui::CreateContext()` e `ImPlot::CreateContext()` hayan sido llamados antes del primer uso.
- Compatible con C++17. No usa excepciones ni RTTI.
+25
View File
@@ -0,0 +1,25 @@
#include "core/sidebar.h"
#include "imgui.h"
static bool s_sidebar_was_open = false;
bool sidebar_begin(const char* title, bool* open, float width) {
if (!*open) {
s_sidebar_was_open = false;
if (ImGui::Button(">")) {
*open = true;
}
return false;
}
s_sidebar_was_open = true;
ImGui::SetNextWindowSize(ImVec2(width, 0.0f), ImGuiCond_Always);
ImGui::Begin(title, open, ImGuiWindowFlags_NoCollapse);
return true;
}
void sidebar_end() {
if (s_sidebar_was_open) {
ImGui::End();
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
// Collapsible sidebar panel.
// Usage:
// if (sidebar_begin("Filters", &sidebar_open)) {
// // draw filter controls
// }
// sidebar_end();
//
// The sidebar renders as a fixed-width ImGui window.
// When collapsed, only a small toggle button is shown.
bool sidebar_begin(const char* title, bool* open, float width = 250.0f);
void sidebar_end();
+59
View File
@@ -0,0 +1,59 @@
---
name: sidebar
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool sidebar_begin(const char* title, bool* open, float width = 250.0f)"
description: "Panel lateral colapsable para filtros y controles de dashboard"
tags: [imgui, sidebar, panel, layout, dashboard, controls]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/sidebar.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del sidebar mostrado en la barra de titulo de la ventana"
- name: open
desc: "Puntero al estado abierto/cerrado; se pone a false si el usuario cierra la ventana"
- name: width
desc: "Ancho del sidebar en pixels (por defecto 250)"
output: "true si el sidebar esta abierto y se debe renderizar contenido entre sidebar_begin y sidebar_end"
---
# sidebar
Panel lateral colapsable. Cuando `*open == true` renderiza una ventana ImGui de ancho fijo con boton de cierre. Cuando `*open == false` muestra un boton compacto ">" para reabrir.
Siempre llamar `sidebar_end()` despues de `sidebar_begin()`, independientemente del valor de retorno.
## Ejemplo
```cpp
static bool filters_open = true;
void render_fn() {
if (sidebar_begin("Filters", &filters_open)) {
ImGui::SliderFloat("Min value", &min_val, 0.0f, 100.0f);
ImGui::Checkbox("Show inactive", &show_inactive);
}
sidebar_end();
// contenido principal
ImGui::Begin("Main");
// ...
ImGui::End();
}
```
## Notas
El estado de `s_sidebar_was_open` es una variable estatica interna que coordina `sidebar_begin` y `sidebar_end`. Solo un sidebar activo a la vez por frame (patron begin/end clasico de ImGui). Para multiples sidebars simultaneos, instanciar logica propia con estado separado.
+21
View File
@@ -0,0 +1,21 @@
#include "core/tab_container.h"
#include "imgui.h"
bool tab_container_begin(const char* id) {
return ImGui::BeginTabBar(
id,
ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown
);
}
bool tab_container_tab(const char* label) {
return ImGui::BeginTabItem(label);
}
void tab_container_tab_end() {
ImGui::EndTabItem();
}
void tab_container_end() {
ImGui::EndTabBar();
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Tab container — wraps ImGui::BeginTabBar with dashboard styling.
// Usage:
// if (tab_container_begin("##views")) {
// if (tab_container_tab("Overview")) {
// // draw overview content
// tab_container_tab_end();
// }
// if (tab_container_tab("Details")) {
// // draw details content
// tab_container_tab_end();
// }
// }
// tab_container_end();
bool tab_container_begin(const char* id);
bool tab_container_tab(const char* label);
void tab_container_tab_end();
void tab_container_end();
+64
View File
@@ -0,0 +1,64 @@
---
name: tab_container
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool tab_container_begin(const char* id)"
description: "Contenedor de tabs para organizar vistas multiples en un dashboard"
tags: [imgui, tabs, container, layout, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/tab_container.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico del tab bar (ej: '##views'). Puede ser invisible con prefijo ##"
- name: label
desc: "Etiqueta visible de cada tab individual"
output: "tab_container_begin: true si el tab bar esta activo. tab_container_tab: true si el tab esta seleccionado y se debe renderizar su contenido"
---
# tab_container
Wrapper fino sobre `ImGui::BeginTabBar` / `EndTabBar` con flags de dashboard preconfigurados: `Reorderable` (arrastrar tabs) y `FittingPolicyResizeDown` (tabs se achican si no caben).
API de cuatro funciones siguiendo el patron begin/end de ImGui. Siempre llamar `tab_container_end()` si `tab_container_begin()` retorno true. Siempre llamar `tab_container_tab_end()` dentro del bloque `if (tab_container_tab(...))`.
## Ejemplo
```cpp
void render_fn() {
ImGui::Begin("Dashboard");
if (tab_container_begin("##main_tabs")) {
if (tab_container_tab("Overview")) {
render_overview();
tab_container_tab_end();
}
if (tab_container_tab("Details")) {
render_details();
tab_container_tab_end();
}
if (tab_container_tab("Settings")) {
render_settings();
tab_container_tab_end();
}
}
tab_container_end();
ImGui::End();
}
```
## Notas
Los flags `ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown` son fijos para estandarizar la experiencia de tabs en dashboards. Si se necesitan flags distintos usar `ImGui::BeginTabBar` directamente.
+90
View File
@@ -0,0 +1,90 @@
#include "time_series_buffer.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <limits>
#include <numeric>
#include <utility>
TimeSeriesBuffer::TimeSeriesBuffer(size_t cap)
: data(new float[cap]), capacity(cap), count(0), offset(0) {}
TimeSeriesBuffer::~TimeSeriesBuffer() {
delete[] data;
}
TimeSeriesBuffer::TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept
: data(other.data), capacity(other.capacity), count(other.count), offset(other.offset) {
other.data = nullptr;
other.capacity = 0;
other.count = 0;
other.offset = 0;
}
TimeSeriesBuffer& TimeSeriesBuffer::operator=(TimeSeriesBuffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
capacity = other.capacity;
count = other.count;
offset = other.offset;
other.data = nullptr;
other.capacity = 0;
other.count = 0;
other.offset = 0;
}
return *this;
}
void TimeSeriesBuffer::push(float value) {
data[offset % capacity] = value;
offset++;
if (count < capacity) count++;
}
float TimeSeriesBuffer::get(size_t index) const {
assert(index < count);
// oldest element is at (offset - count) % capacity
size_t real = (offset - count + index) % capacity;
return data[real];
}
float TimeSeriesBuffer::latest() const {
assert(count > 0);
return data[(offset - 1) % capacity];
}
float TimeSeriesBuffer::min() const {
float m = std::numeric_limits<float>::max();
for (size_t i = 0; i < count; ++i) m = std::min(m, get(i));
return m;
}
float TimeSeriesBuffer::max() const {
float m = std::numeric_limits<float>::lowest();
for (size_t i = 0; i < count; ++i) m = std::max(m, get(i));
return m;
}
float TimeSeriesBuffer::average() const {
if (count == 0) return 0.0f;
float sum = 0.0f;
for (size_t i = 0; i < count; ++i) sum += get(i);
return sum / static_cast<float>(count);
}
size_t TimeSeriesBuffer::size() const {
return count;
}
bool TimeSeriesBuffer::full() const {
return count == capacity;
}
size_t TimeSeriesBuffer::copy_ordered(float* out, size_t out_capacity) const {
size_t n = std::min(count, out_capacity);
for (size_t i = 0; i < n; ++i)
out[i] = get(i);
return n;
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <cstddef>
struct TimeSeriesBuffer {
float* data;
size_t capacity;
size_t count;
size_t offset; // write head
TimeSeriesBuffer(size_t cap);
~TimeSeriesBuffer();
// Non-copyable, moveable
TimeSeriesBuffer(const TimeSeriesBuffer&) = delete;
TimeSeriesBuffer& operator=(const TimeSeriesBuffer&) = delete;
TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept;
TimeSeriesBuffer& operator=(TimeSeriesBuffer&& other) noexcept;
void push(float value);
float get(size_t index) const; // 0 = oldest
float latest() const;
float min() const;
float max() const;
float average() const;
size_t size() const;
bool full() const;
// For ImPlot: copies data in order to a contiguous array
// Returns actual count written
size_t copy_ordered(float* out, size_t out_capacity) const;
};
+59
View File
@@ -0,0 +1,59 @@
---
name: time_series_buffer
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "TimeSeriesBuffer(size_t capacity)"
description: "Ring buffer circular para datos de series temporales, optimizado para streaming de metricas en dashboards en tiempo real"
tags: [buffer, timeseries, streaming, dashboard, data]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/time_series_buffer.cpp"
framework: imgui
params:
- name: capacity
desc: "Numero maximo de muestras que almacena el buffer"
output: "Buffer circular listo para push de datos y lectura ordenada"
---
# time_series_buffer
Ring buffer circular de floats para series temporales en tiempo real. Diseñado para alimentar plots de ImPlot con metricas de streaming (FPS, latencia, uso de CPU, etc.).
## Uso basico
```cpp
TimeSeriesBuffer fps_history(512);
// En el loop principal:
fps_history.push(ImGui::GetIO().Framerate);
// Para renderizar con ImPlot:
float ordered[512];
size_t n = fps_history.copy_ordered(ordered, 512);
ImPlot::PlotLines("FPS", ordered, (int)n);
```
## Semantica del ring buffer
- `push(v)` escribe en `data[offset % capacity]` y avanza el write head.
- `count` crece hasta `capacity` y se mantiene ahi — el buffer nunca desborda.
- `get(0)` retorna el elemento mas antiguo; `get(count-1)` el mas reciente.
- `latest()` es equivalente a `get(count-1)` pero mas explicito.
- `copy_ordered` extrae los datos en orden cronologico para pasarlos a ImPlot u otro consumidor que espere un array contiguo.
## Notas
- No depende de ImGui ni ImPlot directamente — es una estructura de datos pura.
- No es thread-safe. Para uso multihilo, proteger con mutex externo.
- Move semantics implementadas; copy queda eliminada (`= delete`) para evitar copias accidentales del heap.
- `min`, `max` y `average` iteran sobre `count` elementos — O(n). Para buffers grandes en hot paths, considerar mantener un acumulador incremental.
+5
View File
@@ -0,0 +1,5 @@
// tracy_zone.cpp
// All definitions live in tracy_zone.h (macros + constexpr).
// This translation unit exists so the header can be included in any build
// without requiring Tracy — compile with -DTRACY_ENABLE to activate profiling.
#include "core/tracy_zone.h"
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
// Convenience macros for Tracy profiling zones.
// No-op when TRACY_ENABLE is not defined.
// Usage: FN_ZONE("my function") at the top of a scope.
#ifdef TRACY_ENABLE
#include "tracy/Tracy.hpp"
// Named zone (appears as-is in Tracy timeline)
#define FN_ZONE(name) ZoneScopedN(name)
// Named zone with explicit ARGB color
#define FN_ZONE_COLOR(name, color) ZoneScopedNC(name, color)
// Frame boundary marker
#define FN_FRAME_MARK FrameMark
// Plot a scalar value in Tracy's plot view
#define FN_PLOT(name, val) TracyPlot(name, val)
#else
#define FN_ZONE(name) (void)0
#define FN_ZONE_COLOR(name, color) (void)0
#define FN_FRAME_MARK (void)0
#define FN_PLOT(name, val) (void)0
#endif
// Preset ARGB colors for common zone categories.
namespace fn_tracy {
constexpr uint32_t COLOR_RENDER = 0x2196F3; // blue
constexpr uint32_t COLOR_UPDATE = 0x4CAF50; // green
constexpr uint32_t COLOR_IO = 0xFF9800; // orange
constexpr uint32_t COLOR_NETWORK = 0xF44336; // red
constexpr uint32_t COLOR_COMPUTE = 0x9C27B0; // purple
}
+83
View File
@@ -0,0 +1,83 @@
---
name: tracy_zone
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "FN_ZONE(name) / FN_ZONE_COLOR(name, color) / FN_FRAME_MARK / FN_PLOT(name, val)"
description: "Macros y constantes de conveniencia para Tracy profiling zones, compilables sin Tracy"
tags: [tracy, profiling, debug, performance, raii]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/tracy_zone.cpp"
framework: imgui
params:
- name: name
desc: "Nombre de la zona tal como aparece en la timeline de Tracy"
- name: color
desc: "Color ARGB uint32 de la zona en Tracy (opcional, solo FN_ZONE_COLOR)"
output: "Zona Tracy activa durante el scope actual; no-op cuando TRACY_ENABLE no está definido"
---
# tracy_zone
Macros de scope para instrumentar secciones de código con Tracy Profiler. Cuando `TRACY_ENABLE` no está definido (ej. builds de producción) todas las macros se expanden a `(void)0`, sin coste alguno.
## Macros disponibles
| Macro | Descripción |
|---|---|
| `FN_ZONE("name")` | Zona con nombre, color automático |
| `FN_ZONE_COLOR("name", color)` | Zona con nombre y color ARGB explícito |
| `FN_FRAME_MARK` | Marca el límite de frame (eje X de Tracy) |
| `FN_PLOT("name", val)` | Envía un valor escalar al panel de plots |
## Colores predefinidos (`fn_tracy::`)
```cpp
fn_tracy::COLOR_RENDER // 0x2196F3 azul — rendering
fn_tracy::COLOR_UPDATE // 0x4CAF50 verde — game update / logic
fn_tracy::COLOR_IO // 0xFF9800 naranja — I/O disco/red
fn_tracy::COLOR_NETWORK // 0xF44336 rojo — red/HTTP
fn_tracy::COLOR_COMPUTE // 0x9C27B0 morado — compute / shader prep
```
## Ejemplo
```cpp
#include "core/tracy_zone.h"
void render_scene() {
FN_ZONE_COLOR("render_scene", fn_tracy::COLOR_RENDER);
// ... render calls ...
}
void update(float dt) {
FN_ZONE("update");
// ... game logic ...
FN_PLOT("dt_ms", dt * 1000.0f);
}
void main_loop() {
while (running) {
update(dt);
render_scene();
FN_FRAME_MARK;
}
}
```
## Notas
- Compilar con `-DTRACY_ENABLE` y enlazar `TracyClient.cpp` para activar profiling.
- Sin `TRACY_ENABLE` el .cpp es prácticamente vacío — coste cero en producción.
- Los colores son ARGB; si el alpha es 0 Tracy aplica su color automático por zona.
- `FN_ZONE` expande a `ZoneScopedN(name)` de Tracy, que crea el contexto con `__LINE__` como discriminador — es seguro llamar varias veces en el mismo scope.
+81
View File
@@ -0,0 +1,81 @@
#include "viz/candlestick.h"
#include "imgui.h"
#include "implot.h"
void candlestick(const char* title, const double* dates, const double* opens,
const double* closes, const double* lows, const double* highs,
int count, float width_percent, bool tooltip) {
if (count <= 0) return;
// Compute half-width of each candle body in data coordinates.
// Use spacing between consecutive dates when count > 1, else fallback to 0.5.
double spacing = (count > 1) ? (dates[1] - dates[0]) : 1.0;
double half_w = spacing * (double)width_percent * 0.5;
ImPlot::SetupAxes("Date", "Price", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time);
// Auto-fit X axis to the data range with a small margin.
ImPlot::SetupAxisLimits(ImAxis_X1,
dates[0] - spacing,
dates[count - 1] + spacing,
ImGuiCond_Always);
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
ImDrawList* draw = ImPlot::GetPlotDrawList();
const ImU32 col_bull = IM_COL32(0, 200, 80, 255); // green — close >= open
const ImU32 col_bear = IM_COL32(220, 50, 50, 255); // red — close < open
int hovered_idx = -1;
for (int i = 0; i < count; i++) {
double x = dates[i];
double open = opens[i];
double close = closes[i];
double low = lows[i];
double high = highs[i];
bool bullish = (close >= open);
ImU32 col = bullish ? col_bull : col_bear;
// Convert data coordinates to screen pixels.
ImVec2 body_tl = ImPlot::PlotToPixels(x - half_w, bullish ? close : open);
ImVec2 body_br = ImPlot::PlotToPixels(x + half_w, bullish ? open : close);
ImVec2 wick_hi = ImPlot::PlotToPixels(x, high);
ImVec2 wick_lo = ImPlot::PlotToPixels(x, low);
float cx = (body_tl.x + body_br.x) * 0.5f;
// Wick (high-low vertical line).
draw->AddLine(ImVec2(cx, wick_hi.y), ImVec2(cx, wick_lo.y), col, 1.5f);
// Body rectangle (open-close).
// Ensure at least 1px height so flat candles are visible.
if (body_br.y <= body_tl.y + 1.0f) body_br.y = body_tl.y + 1.0f;
draw->AddRectFilled(body_tl, body_br, col);
draw->AddRect(body_tl, body_br, col);
// Track hovered candle for tooltip.
if (tooltip && ImPlot::IsPlotHovered()) {
ImVec2 mouse = ImGui::GetMousePos();
if (mouse.x >= body_tl.x - 4 && mouse.x <= body_br.x + 4 &&
mouse.y >= wick_hi.y - 4 && mouse.y <= wick_lo.y + 4) {
hovered_idx = i;
}
}
}
// Tooltip for the hovered candle.
if (tooltip && hovered_idx >= 0) {
int i = hovered_idx;
ImGui::BeginTooltip();
ImGui::Text("O: %.4f", opens[i]);
ImGui::Text("H: %.4f", highs[i]);
ImGui::Text("L: %.4f", lows[i]);
ImGui::Text("C: %.4f", closes[i]);
ImGui::EndTooltip();
}
ImPlot::EndPlot();
}
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
// Renders an OHLC candlestick chart using ImPlot custom rendering.
// Call within an ImGui frame (inside fn::run_app render callback).
// Green candles when close >= open, red when close < open.
// width_percent controls candle body width as a fraction of inter-candle spacing.
void candlestick(const char* title, const double* dates, const double* opens,
const double* closes, const double* lows, const double* highs,
int count, float width_percent = 0.25f, bool tooltip = true);
+67
View File
@@ -0,0 +1,67 @@
---
name: candlestick
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void candlestick(const char* title, const double* dates, const double* opens, const double* closes, const double* lows, const double* highs, int count, float width_percent = 0.25f, bool tooltip = true)"
description: "Renderiza un grafico de velas OHLC usando ImPlot custom rendering. Verde para velas alcistas (close >= open), rojo para bajistas."
tags: [implot, chart, visualization, gpu, candlestick, ohlc, finance]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot, imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/candlestick.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del grafico, se muestra como header del plot"
- name: dates
desc: "Array de timestamps Unix o indices numericos del eje X, uno por vela"
- name: opens
desc: "Array de precios de apertura, uno por vela"
- name: closes
desc: "Array de precios de cierre, uno por vela"
- name: lows
desc: "Array de precios minimos (punta inferior del wick), uno por vela"
- name: highs
desc: "Array de precios maximos (punta superior del wick), uno por vela"
- name: count
desc: "Numero de velas (longitud de todos los arrays)"
- name: width_percent
desc: "Ancho del body de cada vela como fraccion del espacio entre puntos consecutivos (0.0-1.0, default 0.25)"
- name: tooltip
desc: "Si true, muestra tooltip con valores O/H/L/C al hacer hover sobre una vela"
output: "Renderiza el grafico de velas OHLC en el frame ImGui actual, sin retornar valor"
---
# candlestick
Grafico de velas OHLC completo usando custom rendering de ImPlot. Dibuja body (open-close) y wicks (high-low) por vela usando `ImPlot::GetPlotDrawList()` y `ImPlot::PlotToPixels()` para conversion de coordenadas.
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). El eje X se configura con `ImPlotScale_Time` para timestamps Unix.
Solo tiene overload `double` porque los datos financieros requieren doble precision.
## Ejemplo
```cpp
// arrays de datos financieros (timestamps Unix, precios)
candlestick("BTC/USD", dates, opens, closes, lows, highs, 90);
// sin tooltip, velas mas anchas
candlestick("ETH/USD", dates, opens, closes, lows, highs, 30, 0.6f, false);
```
## Notas
- El ancho de cada vela se calcula como `(dates[1] - dates[0]) * width_percent * 0.5` en cada lado. Asume spacing uniforme entre velas.
- Para un solo punto (`count == 1`) el spacing por defecto es 1.0.
- La deteccion de hover usa un margen de 4px alrededor del area cuerpo+wick para facilitar la interaccion.
- El eje X usa `ImPlotScale_Time` — si los datos son indices numericos simples (0, 1, 2...) en lugar de timestamps, pasar `ImPlotAxisFlags_NoDecorations` o cambiar `SetupAxisScale`.
+94
View File
@@ -0,0 +1,94 @@
#include "viz/gauge.h"
#include "imgui.h"
#include <cmath>
#include <cstdio>
#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif
void gauge(const char* label, float value, float min_val, float max_val, float radius) {
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 pos = ImGui::GetCursorScreenPos();
// Reserve space: diameter + label line
float diameter = radius * 2.0f;
ImGui::Dummy(ImVec2(diameter, diameter + ImGui::GetTextLineHeightWithSpacing()));
ImVec2 center = ImVec2(pos.x + radius, pos.y + radius);
// Arc spans 240 degrees: from 150deg to 390deg (i.e. 150 to 30 going clockwise)
// In screen space Y is down, so angles go clockwise.
// Start angle: 150 degrees = bottom-left, End angle: 390 = 30 degrees = bottom-right
const float angle_start = (150.0f * (float)M_PI) / 180.0f;
const float angle_end = (390.0f * (float)M_PI) / 180.0f;
const int num_segments = 64;
// Background arc (dark gray)
ImU32 bg_color = IM_COL32(60, 60, 60, 220);
for (int i = 0; i < num_segments; i++) {
float a0 = angle_start + (angle_end - angle_start) * ((float)i / num_segments);
float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
draw_list->AddLine(
ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
bg_color, 6.0f);
}
// Normalize value to [0, 1]
float t = 0.0f;
if (max_val > min_val) {
t = (value - min_val) / (max_val - min_val);
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
}
// Color: green (t=0) -> yellow (t=0.5) -> red (t=1)
float r, g, b;
if (t < 0.5f) {
float s = t * 2.0f;
r = (unsigned char)(s * 255);
g = 200;
b = 0;
} else {
float s = (t - 0.5f) * 2.0f;
r = 220;
g = (unsigned char)((1.0f - s) * 200);
b = 0;
}
ImU32 value_color = IM_COL32((int)r, (int)g, (int)b, 255);
// Value arc
float angle_value = angle_start + (angle_end - angle_start) * t;
int value_segments = (int)(num_segments * t);
for (int i = 0; i < value_segments; i++) {
float a0 = angle_start + (angle_end - angle_start) * ((float)i / num_segments);
float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
draw_list->AddLine(
ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
value_color, 6.0f);
}
// Needle: line from center to arc at current angle
float needle_len = radius * 0.75f;
ImVec2 needle_tip = ImVec2(
center.x + cosf(angle_value) * needle_len,
center.y + sinf(angle_value) * needle_len);
draw_list->AddLine(center, needle_tip, IM_COL32(255, 255, 255, 240), 2.0f);
draw_list->AddCircleFilled(center, 4.0f, IM_COL32(255, 255, 255, 200));
// Value text centered
char val_buf[32];
snprintf(val_buf, sizeof(val_buf), "%.1f", value);
ImVec2 val_size = ImGui::CalcTextSize(val_buf);
draw_list->AddText(
ImVec2(center.x - val_size.x * 0.5f, center.y + radius * 0.35f),
IM_COL32(230, 230, 230, 255), val_buf);
// Label below value
ImVec2 label_size = ImGui::CalcTextSize(label);
draw_list->AddText(
ImVec2(center.x - label_size.x * 0.5f, center.y + radius * 0.35f + val_size.y + 2.0f),
IM_COL32(180, 180, 180, 200), label);
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Renders a circular gauge/speedometer indicator using ImGui draw primitives.
// Call within an ImGui frame.
// color is interpolated green->yellow->red based on normalized value.
void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f);
+59
View File
@@ -0,0 +1,59 @@
---
name: gauge
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f)"
description: "Renderiza un indicador circular tipo gauge/velocimetro usando ImGui draw primitives"
tags: [imgui, visualization, gauge, kpi, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/gauge.cpp"
framework: imgui
params:
- name: label
desc: "Etiqueta del gauge, se muestra centrada debajo del valor numerico"
- name: value
desc: "Valor actual a mostrar en el gauge"
- name: min_val
desc: "Valor minimo de la escala (extremo izquierdo del arco)"
- name: max_val
desc: "Valor maximo de la escala (extremo derecho del arco)"
- name: radius
desc: "Radio del gauge en pixels (default 60.0)"
output: "Renderiza el gauge en el frame ImGui actual, reservando espacio con ImGui::Dummy"
---
# gauge
Indicador circular tipo gauge/velocimetro construido sobre ImGui draw primitives. No requiere ImPlot.
El arco ocupa 240 grados (de 150deg a 390deg en sentido horario). El color del arco de valor interpolado de verde (minimo) a amarillo (mitad) a rojo (maximo). Una aguja blanca apunta al valor actual.
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
// KPI card con gauge de temperatura
gauge("CPU Temp", 72.5f, 0.0f, 100.0f, 50.0f);
// Gauge grande para dashboard principal
gauge("Velocidad", 3200.0f, 0.0f, 5000.0f, 80.0f);
```
## Notas
- El arco de fondo es gris oscuro (IM_COL32(60,60,60,220)), 6px de grosor.
- La aguja tiene longitud del 75% del radio para evitar solapar el arco.
- Usa solo `float`; no ofrece overload `double` porque ImGui DrawList trabaja en coordenadas de pantalla (float).
- El espacio reservado es `diameter x (diameter + line_height)` para incluir la etiqueta.
+353
View File
@@ -0,0 +1,353 @@
#include "viz/graph_force_layout.h"
#include "viz/graph_types.h"
#include <cmath>
#include <cstdlib>
#include <algorithm>
// ---------------------------------------------------------------------------
// Quadtree for Barnes-Hut approximation
// ---------------------------------------------------------------------------
struct QuadNode {
float cx, cy; // center of mass
float mass; // total mass (node count in subtree)
float x0, y0; // bounding box min
float x1, y1; // bounding box max
int children[4]; // NW=0, NE=1, SW=2, SE=3 (-1 = empty)
int body; // node index if leaf (-1 if internal)
};
static constexpr int MAX_QUAD_NODES = 1 << 20; // supports graphs up to ~1M nodes
static QuadNode quad_pool[MAX_QUAD_NODES];
static int quad_count = 0;
static int quad_new(float x0, float y0, float x1, float y1) {
if (quad_count >= MAX_QUAD_NODES) return -1;
int idx = quad_count++;
QuadNode& q = quad_pool[idx];
q.cx = 0; q.cy = 0; q.mass = 0;
q.x0 = x0; q.y0 = y0; q.x1 = x1; q.y1 = y1;
q.children[0] = q.children[1] = q.children[2] = q.children[3] = -1;
q.body = -1;
return idx;
}
// Determine quadrant index for point (px,py) relative to cell midpoint.
// 0=NW, 1=NE, 2=SW, 3=SE
static int quad_child_idx(const QuadNode& q, float px, float py) {
float mx = (q.x0 + q.x1) * 0.5f;
float my = (q.y0 + q.y1) * 0.5f;
int xi = (px >= mx) ? 1 : 0;
int yi = (py >= my) ? 2 : 0;
return xi | yi;
}
// Subdivide cell qi into four children.
static void quad_subdivide(int qi) {
QuadNode& q = quad_pool[qi];
float mx = (q.x0 + q.x1) * 0.5f;
float my = (q.y0 + q.y1) * 0.5f;
// NW
quad_pool[qi].children[0] = quad_new(q.x0, q.y0, mx, my);
// NE
quad_pool[qi].children[1] = quad_new(mx, q.y0, q.x1, my);
// SW
quad_pool[qi].children[2] = quad_new(q.x0, my, mx, q.y1);
// SE
quad_pool[qi].children[3] = quad_new(mx, my, q.x1, q.y1);
}
// Insert body (node_idx at position nx,ny with mass nmass) into cell qi.
// Uses iterative descent to avoid stack overflow on deep trees.
static void quad_insert(int root, int node_idx, float nx, float ny, float nmass) {
int qi = root;
while (qi >= 0) {
QuadNode& q = quad_pool[qi];
// Update center of mass
float total = q.mass + nmass;
q.cx = (q.cx * q.mass + nx * nmass) / total;
q.cy = (q.cy * q.mass + ny * nmass) / total;
q.mass = total;
if (q.body == -1 && q.children[0] == -1) {
// Empty leaf: place body here
q.body = node_idx;
return;
}
if (q.body >= 0) {
// Leaf with existing body: subdivide, push existing body down
quad_subdivide(qi);
// Move old body into correct child (re-read q after subdivide since pool may shift)
QuadNode& qq = quad_pool[qi];
int old_body = qq.body;
float obx = /* we need positions */ 0, oby = 0;
// We store positions in the GraphData, pass via closure is not possible here.
// Instead we pass a pointer to positions alongside. We'll fix this by using
// a file-scope pointer set before each build.
(void)old_body; (void)obx; (void)oby;
// NOTE: positions accessed via file-scope g_nodes pointer below.
qq.body = -1;
}
int ci = quad_child_idx(quad_pool[qi], nx, ny);
qi = quad_pool[qi].children[ci];
}
}
// File-scope pointers set before each tree build (avoids passing them everywhere).
static const GraphNode* g_nodes = nullptr;
// Insert body knowing positions from g_nodes.
static void quad_insert_body(int qi, int node_idx) {
float nx = g_nodes[node_idx].x;
float ny = g_nodes[node_idx].y;
const float nmass = 1.0f;
while (qi >= 0) {
QuadNode& q = quad_pool[qi];
float total = q.mass + nmass;
q.cx = (q.cx * q.mass + nx * nmass) / total;
q.cy = (q.cy * q.mass + ny * nmass) / total;
q.mass = total;
if (q.body == -1 && q.children[0] == -1) {
// Empty leaf
q.body = node_idx;
return;
}
if (q.children[0] == -1) {
// Leaf occupied: subdivide and push existing body down
int old_body = q.body;
q.body = -1;
quad_subdivide(qi);
// Push old body into child
int old_ci = quad_child_idx(quad_pool[qi], g_nodes[old_body].x, g_nodes[old_body].y);
int old_child = quad_pool[qi].children[old_ci];
if (old_child >= 0) {
QuadNode& oc = quad_pool[old_child];
oc.cx = g_nodes[old_body].x;
oc.cy = g_nodes[old_body].y;
oc.mass = 1.0f;
oc.body = old_body;
}
}
int ci = quad_child_idx(quad_pool[qi], nx, ny);
qi = quad_pool[qi].children[ci];
}
}
// Compute Barnes-Hut repulsion force on node at (nx,ny) from subtree qi.
// Accumulates force into (fx, fy).
static void quad_force(int qi, float nx, float ny,
float theta, float repulsion, float min_dist,
float& fx, float& fy) {
// Iterative traversal using a small stack to avoid recursion depth issues.
static int stack[MAX_QUAD_NODES]; // reuse static stack
int top = 0;
stack[top++] = qi;
while (top > 0) {
int ci = stack[--top];
if (ci < 0) continue;
const QuadNode& q = quad_pool[ci];
if (q.mass == 0) continue;
float dx = q.cx - nx;
float dy = q.cy - ny;
float dist2 = dx * dx + dy * dy;
float dist = std::sqrt(dist2);
if (dist < min_dist) dist = min_dist;
// Cell size
float cell_size = q.x1 - q.x0;
// Use multipole approximation if far enough OR if leaf
bool is_leaf = (q.children[0] == -1);
if (is_leaf || (cell_size / dist) < theta) {
// Coulomb repulsion: F = repulsion * mass / dist^2
float force = repulsion * q.mass / (dist * dist);
fx -= force * dx / dist;
fy -= force * dy / dist;
} else {
// Push children
for (int k = 0; k < 4; ++k) {
if (q.children[k] >= 0)
stack[top++] = q.children[k];
}
}
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config) {
if (graph.node_count <= 0) return 0.0f;
// Temporary force accumulators (stack-allocated for small graphs, static for large)
static float* fx_buf = nullptr;
static float* fy_buf = nullptr;
static int buf_cap = 0;
if (graph.node_count > buf_cap) {
delete[] fx_buf;
delete[] fy_buf;
buf_cap = graph.node_count + 64;
fx_buf = new float[buf_cap];
fy_buf = new float[buf_cap];
}
float total_energy = 0.0f;
for (int iter = 0; iter < config.iterations; ++iter) {
// Zero forces
for (int i = 0; i < graph.node_count; ++i) {
fx_buf[i] = 0.0f;
fy_buf[i] = 0.0f;
}
// ---- Build Barnes-Hut quadtree ----
// Compute bounding box of current positions
float bx0 = graph.nodes[0].x, bx1 = graph.nodes[0].x;
float by0 = graph.nodes[0].y, by1 = graph.nodes[0].y;
for (int i = 1; i < graph.node_count; ++i) {
float px = graph.nodes[i].x, py = graph.nodes[i].y;
if (px < bx0) bx0 = px; if (px > bx1) bx1 = px;
if (py < by0) by0 = py; if (py > by1) by1 = py;
}
// Add margin to avoid degeneracies
float margin = (bx1 - bx0 + by1 - by0) * 0.05f + 1.0f;
bx0 -= margin; bx1 += margin;
by0 -= margin; by1 += margin;
// Make it square
float side = std::max(bx1 - bx0, by1 - by0);
float cx = (bx0 + bx1) * 0.5f, cy = (by0 + by1) * 0.5f;
bx0 = cx - side * 0.5f; bx1 = cx + side * 0.5f;
by0 = cy - side * 0.5f; by1 = cy + side * 0.5f;
quad_count = 0;
g_nodes = graph.nodes;
int root = quad_new(bx0, by0, bx1, by1);
for (int i = 0; i < graph.node_count; ++i) {
quad_insert_body(root, i);
}
// ---- Repulsion via Barnes-Hut ----
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
quad_force(root,
graph.nodes[i].x, graph.nodes[i].y,
config.theta, config.repulsion, config.min_distance,
fx_buf[i], fy_buf[i]);
// Subtract self-interaction (the tree includes the node itself)
// Self-force: repulsion * 1 / min_dist^2, but direction is (0,0) -> skip
}
// ---- Attraction along edges (spring force) ----
for (int e = 0; e < graph.edge_count; ++e) {
const GraphEdge& edge = graph.edges[e];
int s = (int)edge.source;
int t = (int)edge.target;
if (s < 0 || s >= graph.node_count) continue;
if (t < 0 || t >= graph.node_count) continue;
float dx = graph.nodes[t].x - graph.nodes[s].x;
float dy = graph.nodes[t].y - graph.nodes[s].y;
float dist = std::sqrt(dx * dx + dy * dy);
if (dist < config.min_distance) dist = config.min_distance;
// F = k * dist * weight (Hooke: pulls toward equilibrium at 0)
float force = config.attraction * dist * edge.weight;
float fx_e = force * dx / dist;
float fy_e = force * dy / dist;
if (!graph.nodes[s].pinned) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; }
if (!graph.nodes[t].pinned) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; }
}
// ---- Gravity toward center (0,0) ----
if (config.gravity != 0.0f) {
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
fx_buf[i] -= config.gravity * graph.nodes[i].x;
fy_buf[i] -= config.gravity * graph.nodes[i].y;
}
}
// ---- Integrate: v = v * damping + F; pos += v ----
total_energy = 0.0f;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
n.vx = n.vx * config.damping + fx_buf[i];
n.vy = n.vy * config.damping + fy_buf[i];
// Clamp velocity
n.vx = std::max(-config.max_velocity, std::min(config.max_velocity, n.vx));
n.vy = std::max(-config.max_velocity, std::min(config.max_velocity, n.vy));
n.x += n.vx;
n.y += n.vy;
total_energy += n.vx * n.vx + n.vy * n.vy;
}
}
graph.update_bounds();
return total_energy;
}
void graph_force_layout_reset(GraphData& graph, float spread) {
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
// rand() produces [0, RAND_MAX]; map to [-spread, spread]
n.x = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
n.y = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
n.vx = 0.0f;
n.vy = 0.0f;
}
graph.update_bounds();
}
void graph_layout_circular(GraphData& graph, float radius) {
if (graph.node_count <= 0) return;
const float two_pi = 6.28318530718f;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
float angle = two_pi * (float)i / (float)graph.node_count;
n.x = radius * std::cos(angle);
n.y = radius * std::sin(angle);
n.vx = 0.0f;
n.vy = 0.0f;
}
graph.update_bounds();
}
void graph_layout_grid(GraphData& graph, float spacing) {
if (graph.node_count <= 0) return;
int cols = (int)std::ceil(std::sqrt((float)graph.node_count));
int rows = (graph.node_count + cols - 1) / cols;
float ox = -0.5f * (cols - 1) * spacing;
float oy = -0.5f * (rows - 1) * spacing;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
int col = i % cols;
int row = i / cols;
n.x = ox + col * spacing;
n.y = oy + row * spacing;
n.vx = 0.0f;
n.vy = 0.0f;
}
graph.update_bounds();
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
struct GraphData; // forward declare
struct ForceLayoutConfig {
float repulsion = 500.0f; // repulsion strength between all nodes
float attraction = 0.01f; // spring constant for edges
float damping = 0.85f; // velocity decay per step
float min_distance = 1.0f; // minimum distance (avoid division by zero)
float theta = 0.5f; // Barnes-Hut threshold (0 = exact, 1 = fast)
float gravity = 0.1f; // pull toward center (prevents drift)
float max_velocity = 50.0f; // cap velocity per axis
int iterations = 1; // steps per call
};
// Perform one (or more) steps of force-directed layout.
// Modifies node positions (x, y) and velocities (vx, vy) in-place.
// Returns the total kinetic energy (sum of |v|^2). When energy < threshold,
// layout has converged.
float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config = {});
// Reset: randomize positions within [-spread, spread], zero velocities.
void graph_force_layout_reset(GraphData& graph, float spread = 200.0f);
// Preset layouts (non-iterative, instant positioning)
void graph_layout_circular(GraphData& graph, float radius = 100.0f);
void graph_layout_grid(GraphData& graph, float spacing = 20.0f);
+79
View File
@@ -0,0 +1,79 @@
---
name: graph_force_layout
kind: function
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)"
description: "Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada"
tags: [graph, layout, force-directed, barnes-hut, physics, gpu]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/graph_force_layout.cpp"
framework: imgui
params:
- name: graph
desc: "Referencia al grafo (GraphData) cuyos nodos se actualizan in-place. Modifica x, y, vx, vy de cada nodo no pinned."
- name: config
desc: "Parametros de la simulacion: repulsion (fuerza coulombiana), attraction (spring constant), damping (decay de velocidad), theta (precision Barnes-Hut 0=exacto/1=rapido), gravity (atraccion al centro), max_velocity, iterations."
output: "Energia cinetica total (suma de |v|^2). Cuando cae por debajo de un umbral elegido por el caller, el layout ha convergido y se puede dejar de llamar."
---
# graph_force_layout
Implementa el algoritmo de layout force-directed clasico (Fruchterman-Reingold / Eades) con aproximacion Barnes-Hut O(n log n) para escalar a grafos de miles de nodos.
## Algoritmo
Cada llamada a `graph_force_layout_step` ejecuta `config.iterations` pasos. Un paso:
1. **Construccion del quadtree** (Barnes-Hut): se calcula el bounding box de las posiciones actuales, se construye un quadtree flat en `quad_pool` (sin allocaciones por nodo). Cada celda acumula centro de masa y masa total.
2. **Repulsion**: para cada nodo se recorre el quadtree. Si el cociente `cell_size / distance < theta`, la celda se trata como una sola masa puntual (multipolo de orden 0). Si no, se desciende a los hijos. Con `theta=0` es O(n²) exacto; con `theta=0.5` es O(n log n).
3. **Atraccion**: para cada arista `(s, t)`, fuerza de Hooke `F = k * dist * weight` en la direccion del arco.
4. **Gravedad**: fuerza proporcional a la distancia al origen, evita que el grafo derive fuera de pantalla.
5. **Integracion**: `v = v * damping + F`, `pos += v`, con clamping de velocidad.
6. Nodos con `pinned = true` no se mueven en ningun paso.
## Funciones auxiliares
```cpp
// Randomizar posiciones para empezar la simulacion
graph_force_layout_reset(graph, 200.0f);
// Layout circular instantaneo (sin iteracion)
graph_layout_circular(graph, 150.0f);
// Layout en grid instantaneo
graph_layout_grid(graph, 25.0f);
```
## Ejemplo de uso tipico (loop ImGui)
```cpp
static ForceLayoutConfig cfg;
static bool running = true;
if (running) {
float energy = graph_force_layout_step(my_graph, cfg);
if (energy < 0.01f) running = false; // convergido
}
```
## Notas de implementacion
- El quadtree usa un pool estatico de `1 << 20` (~1M) celdas. Para grafos de >500K nodos
se recomienda reducir `MAX_QUAD_NODES` o aumentarlo segun memoria disponible.
- La pila de traversal en `quad_force` es tambien estatica (`static int stack[]`); no es
thread-safe si se llama desde multiples hilos simultaneamente.
- `graph_force_layout_reset` usa `rand()`. Para reproducibilidad llama `srand(seed)` antes.
- Los buffers de fuerza (`fx_buf`, `fy_buf`) se realocan una sola vez cuando el conteo de
nodos supera la capacidad previa; en el uso normal (tamano fijo) no hay allocaciones
por frame.
+446
View File
@@ -0,0 +1,446 @@
#include "viz/graph_renderer.h"
#include "viz/graph_types.h"
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
// ---------------------------------------------------------------------------
// Community palette (ABGR packed, 10 colors)
// ---------------------------------------------------------------------------
static const uint32_t k_palette[10] = {
0xFF4CAF50, // green
0xFFF44336, // red
0xFF2196F3, // blue
0xFFFF9800, // orange
0xFF9C27B0, // purple
0xFF00BCD4, // cyan
0xFFFFEB3B, // yellow
0xFFE91E63, // pink
0xFF795548, // brown
0xFF607D8B // blue-grey
};
// ---------------------------------------------------------------------------
// Internal struct
// ---------------------------------------------------------------------------
struct GraphRenderer {
unsigned int fbo;
unsigned int texture;
unsigned int rbo; // depth/stencil renderbuffer
int width, height;
// Node rendering (instanced quads)
unsigned int node_vao, node_quad_vbo, node_instance_vbo;
unsigned int node_shader;
// Edge rendering (lines)
unsigned int edge_vao, edge_vbo;
unsigned int edge_shader;
GraphRendererConfig config;
};
// ---------------------------------------------------------------------------
// Shader sources
// ---------------------------------------------------------------------------
// Node vertex shader — instanced unit quad
static const char* k_node_vert = R"(
#version 330 core
// Quad corners [-0.5, 0.5]
layout(location = 0) in vec2 a_quad;
// Per-instance: world position, size, RGBA color
layout(location = 1) in vec2 a_pos;
layout(location = 2) in float a_size;
layout(location = 3) in vec4 a_color;
out vec2 v_uv;
out vec4 v_color;
uniform vec2 u_viewport; // (width, height) in pixels
uniform float u_scale; // cam_zoom
uniform vec2 u_translate; // (tx, ty) in pixels
void main() {
// World -> screen (pixels)
vec2 screen = a_pos * u_scale + u_translate;
// Expand quad by node radius (size = diameter)
screen += a_quad * a_size * u_scale;
// Screen -> NDC
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
ndc.y = -ndc.y; // flip Y (screen Y grows downward)
gl_Position = vec4(ndc, 0.0, 1.0);
v_uv = a_quad + 0.5; // [0,1]
v_color = a_color;
}
)";
// Node fragment shader — SDF circle with outline
static const char* k_node_frag = R"(
#version 330 core
in vec2 v_uv;
in vec4 v_color;
out vec4 frag_color;
uniform float u_outline_px; // outline width in uv units
uniform float u_node_px; // node diameter in pixels (= size * zoom)
void main() {
// SDF circle centered at (0.5, 0.5) in uv space
float dist = length(v_uv - 0.5);
float r = 0.5;
// Anti-alias edge (in uv units; 1px ~ 1/u_node_px in uv space)
float fwidth_uv = 1.5 / max(u_node_px, 1.0);
float alpha = 1.0 - smoothstep(r - fwidth_uv, r, dist);
if (alpha < 0.001) discard;
// Outline ring
float outline_uv = u_outline_px / max(u_node_px, 1.0);
float outline = smoothstep(r - outline_uv - fwidth_uv, r - outline_uv, dist);
vec3 fill = v_color.rgb;
vec3 outline_col = mix(fill, vec3(1.0), 0.6); // lighter outline
vec3 color = mix(fill, outline_col, outline);
frag_color = vec4(color, v_color.a * alpha);
}
)";
// Edge vertex shader
static const char* k_edge_vert = R"(
#version 330 core
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec4 a_color;
out vec4 v_color;
uniform vec2 u_viewport;
uniform float u_scale;
uniform vec2 u_translate;
void main() {
vec2 screen = a_pos * u_scale + u_translate;
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
v_color = a_color;
}
)";
// Edge fragment shader
static const char* k_edge_frag = R"(
#version 330 core
in vec4 v_color;
out vec4 frag_color;
void main() {
frag_color = v_color;
}
)";
// ---------------------------------------------------------------------------
// Shader helpers
// ---------------------------------------------------------------------------
static unsigned int compile_shader(GLenum type, const char* src) {
unsigned int s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
int ok;
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char buf[512];
glGetShaderInfoLog(s, sizeof(buf), nullptr, buf);
fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf);
}
return s;
}
static unsigned int link_program(const char* vert_src, const char* frag_src) {
unsigned int vs = compile_shader(GL_VERTEX_SHADER, vert_src);
unsigned int fs = compile_shader(GL_FRAGMENT_SHADER, frag_src);
unsigned int prog = glCreateProgram();
glAttachShader(prog, vs);
glAttachShader(prog, fs);
glLinkProgram(prog);
int ok;
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) {
char buf[512];
glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf);
fprintf(stderr, "[graph_renderer] program link error: %s\n", buf);
}
glDeleteShader(vs);
glDeleteShader(fs);
return prog;
}
// ---------------------------------------------------------------------------
// FBO helpers
// ---------------------------------------------------------------------------
static void create_fbo(GraphRenderer* r) {
// Texture
glGenTextures(1, &r->texture);
glBindTexture(GL_TEXTURE_2D, r->texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, r->width, r->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
// Depth renderbuffer
glGenRenderbuffers(1, &r->rbo);
glBindRenderbuffer(GL_RENDERBUFFER, r->rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, r->width, r->height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// FBO
glGenFramebuffers(1, &r->fbo);
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, r->texture, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, r->rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
static void destroy_fbo(GraphRenderer* r) {
glDeleteFramebuffers(1, &r->fbo);
glDeleteTextures(1, &r->texture);
glDeleteRenderbuffers(1, &r->rbo);
r->fbo = r->texture = r->rbo = 0;
}
// ---------------------------------------------------------------------------
// Helper: unpack ABGR uint32 to float RGBA
// ---------------------------------------------------------------------------
static inline void abgr_to_rgba(uint32_t abgr, float& r, float& g, float& b, float& a) {
// ABGR layout: bits 31-24 = A, 23-16 = B, 15-8 = G, 7-0 = R
a = ((abgr >> 24) & 0xFF) / 255.0f;
b = ((abgr >> 16) & 0xFF) / 255.0f;
g = ((abgr >> 8) & 0xFF) / 255.0f;
r = ((abgr ) & 0xFF) / 255.0f;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config) {
GraphRenderer* r = new GraphRenderer();
r->width = width;
r->height = height;
r->config = config;
// --- FBO ---
create_fbo(r);
// --- Node VAO ---
// Unit quad: 4 vertices, each (x, y) in [-0.5, 0.5]
static const float quad_verts[8] = {
-0.5f, -0.5f,
0.5f, -0.5f,
-0.5f, 0.5f,
0.5f, 0.5f,
};
glGenVertexArrays(1, &r->node_vao);
glBindVertexArray(r->node_vao);
// Quad VBO (location 0)
glGenBuffers(1, &r->node_quad_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
// Instance VBO (location 1,2,3 — position, size, color)
glGenBuffers(1, &r->node_instance_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
// layout: x, y, size, r, g, b, a — 7 floats per instance
glEnableVertexAttribArray(1); // pos
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0);
glVertexAttribDivisor(1, 1);
glEnableVertexAttribArray(2); // size
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(2 * sizeof(float)));
glVertexAttribDivisor(2, 1);
glEnableVertexAttribArray(3); // color
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float)));
glVertexAttribDivisor(3, 1);
glBindVertexArray(0);
// --- Edge VAO ---
// Each edge: 2 vertices x (x, y, r, g, b, a) = 2 * 6 floats
glGenVertexArrays(1, &r->edge_vao);
glBindVertexArray(r->edge_vao);
glGenBuffers(1, &r->edge_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
glEnableVertexAttribArray(0); // pos
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1); // color
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// --- Shaders ---
r->node_shader = link_program(k_node_vert, k_node_frag);
r->edge_shader = link_program(k_edge_vert, k_edge_frag);
return r;
}
void graph_renderer_destroy(GraphRenderer* r) {
if (!r) return;
destroy_fbo(r);
glDeleteVertexArrays(1, &r->node_vao);
glDeleteBuffers(1, &r->node_quad_vbo);
glDeleteBuffers(1, &r->node_instance_vbo);
glDeleteVertexArrays(1, &r->edge_vao);
glDeleteBuffers(1, &r->edge_vbo);
glDeleteProgram(r->node_shader);
glDeleteProgram(r->edge_shader);
delete r;
}
void graph_renderer_resize(GraphRenderer* r, int width, int height) {
if (!r) return;
if (r->width == width && r->height == height) return;
r->width = width;
r->height = height;
destroy_fbo(r);
create_fbo(r);
}
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom) {
if (!r) return 0;
// --- Save GL state ---
GLint prev_fbo;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
GLint prev_viewport[4];
glGetIntegerv(GL_VIEWPORT, prev_viewport);
// --- Bind FBO ---
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
glViewport(0, 0, r->width, r->height);
// Clear with bg_color (ABGR)
float bg_a, bg_b, bg_g, bg_cr;
abgr_to_rgba(r->config.bg_color, bg_cr, bg_g, bg_b, bg_a);
glClearColor(bg_cr, bg_g, bg_b, bg_a);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Enable blending for anti-aliasing and transparency
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// View transform: world -> screen pixels
// tx = -cam_x * scale + width/2
// ty = -cam_y * scale + height/2
float scale = cam_zoom;
float tx = -cam_x * scale + (float)r->width * 0.5f;
float ty = -cam_y * scale + (float)r->height * 0.5f;
// ----------------------------------------------------------------
// Draw edges
// ----------------------------------------------------------------
if (graph.edge_count > 0 && graph.edges && graph.nodes) {
// Pack: 2 vertices per edge, each vertex = (x, y, r, g, b, a) = 6 floats
const int floats_per_edge = 2 * 6;
float* edge_buf = (float*)malloc((size_t)graph.edge_count * floats_per_edge * sizeof(float));
int vi = 0;
for (int i = 0; i < graph.edge_count; ++i) {
const GraphEdge& e = graph.edges[i];
uint32_t ecol = e.color != 0 ? e.color : 0xFF888888u; // default gray
float er, eg, eb, ea;
abgr_to_rgba(ecol, er, eg, eb, ea);
ea *= r->config.edge_alpha;
if (e.source < (uint32_t)graph.node_count && e.target < (uint32_t)graph.node_count) {
const GraphNode& ns = graph.nodes[e.source];
const GraphNode& nt = graph.nodes[e.target];
// Source vertex
edge_buf[vi++] = ns.x; edge_buf[vi++] = ns.y;
edge_buf[vi++] = er; edge_buf[vi++] = eg;
edge_buf[vi++] = eb; edge_buf[vi++] = ea;
// Target vertex
edge_buf[vi++] = nt.x; edge_buf[vi++] = nt.y;
edge_buf[vi++] = er; edge_buf[vi++] = eg;
edge_buf[vi++] = eb; edge_buf[vi++] = ea;
}
}
glUseProgram(r->edge_shader);
glUniform2f(glGetUniformLocation(r->edge_shader, "u_viewport"), (float)r->width, (float)r->height);
glUniform1f(glGetUniformLocation(r->edge_shader, "u_scale"), scale);
glUniform2f(glGetUniformLocation(r->edge_shader, "u_translate"), tx, ty);
glLineWidth(r->config.edge_width);
glBindVertexArray(r->edge_vao);
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
glBufferData(GL_ARRAY_BUFFER, vi * (int)sizeof(float), edge_buf, GL_DYNAMIC_DRAW);
glDrawArrays(GL_LINES, 0, vi / 6);
glBindVertexArray(0);
free(edge_buf);
}
// ----------------------------------------------------------------
// Draw nodes (instanced quads)
// ----------------------------------------------------------------
if (graph.node_count > 0 && graph.nodes) {
// Pack: 7 floats per node: x, y, size, r, g, b, a
float* node_buf = (float*)malloc((size_t)graph.node_count * 7 * sizeof(float));
for (int i = 0; i < graph.node_count; ++i) {
const GraphNode& n = graph.nodes[i];
uint32_t ncol = n.color != 0 ? n.color : k_palette[n.community % 10];
float nr, ng, nb, na;
abgr_to_rgba(ncol, nr, ng, nb, na);
float sz = n.size > 0.0f ? n.size : 4.0f;
float* p = node_buf + i * 7;
p[0] = n.x; p[1] = n.y; p[2] = sz;
p[3] = nr; p[4] = ng; p[5] = nb; p[6] = na;
}
glUseProgram(r->node_shader);
glUniform2f(glGetUniformLocation(r->node_shader, "u_viewport"), (float)r->width, (float)r->height);
glUniform1f(glGetUniformLocation(r->node_shader, "u_scale"), scale);
glUniform2f(glGetUniformLocation(r->node_shader, "u_translate"), tx, ty);
glUniform1f(glGetUniformLocation(r->node_shader, "u_outline_px"), r->config.node_outline);
glBindVertexArray(r->node_vao);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
glBufferData(GL_ARRAY_BUFFER, graph.node_count * 7 * (int)sizeof(float), node_buf, GL_DYNAMIC_DRAW);
// Draw 4 vertices (triangle strip quad) x node_count instances
// Pass per-instance node_px uniform via the average size (approximation)
// For exact per-node pixel size we'd need a texture or another approach;
// use a uniform average for AA quality — good enough for most graphs.
float avg_px = 8.0f * scale; // rough estimate
glUniform1f(glGetUniformLocation(r->node_shader, "u_node_px"), avg_px);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, graph.node_count);
glBindVertexArray(0);
free(node_buf);
}
// --- Restore GL state ---
glDisable(GL_BLEND);
glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
return r->texture;
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
struct GraphData; // forward declare
struct GraphRendererConfig {
float node_outline = 1.5f; // outline width in pixels
float edge_width = 1.0f; // edge line width
float edge_alpha = 0.4f; // edge transparency
uint32_t bg_color = 0xFF1A1A1E; // ABGR background
bool edge_fade_alpha = true; // fade edge alpha by distance to camera
// Default palette for communities (when node.color == 0)
// 10 distinct colors, ABGR packed
};
struct GraphRenderer;
// Lifecycle
GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config = {});
void graph_renderer_destroy(GraphRenderer* r);
void graph_renderer_resize(GraphRenderer* r, int width, int height);
// Render graph to internal FBO.
// cam_x, cam_y: camera center in graph space
// cam_zoom: zoom level (1.0 = 1:1 pixel mapping)
// Returns OpenGL texture ID suitable for ImGui::Image().
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom);
+87
View File
@@ -0,0 +1,87 @@
---
name: graph_renderer
kind: function
lang: cpp
domain: viz
version: "1.0.0"
purity: impure
signature: "GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)"
description: "Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes"
tags: [graph, renderer, opengl, gpu, instanced, fbo, visualization]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/graph_renderer.cpp"
framework: imgui
params:
- name: width
desc: "Ancho del framebuffer en pixels"
- name: height
desc: "Alto del framebuffer en pixels"
- name: config
desc: "Configuracion visual: outline width, edge width, edge alpha, color de fondo, fade de aristas por distancia a camara"
output: "Handle opaco al renderer. Usar graph_renderer_draw() para obtener texture ID de OpenGL, pasable directamente a ImGui::Image()"
---
# graph_renderer
Renderer GPU de grafos basado en OpenGL 3.3 core profile con instanced rendering. Renderiza nodos y aristas de un `GraphData` a un FBO interno y retorna el texture ID para integracion directa con `ImGui::Image()`.
## Funciones del API
```cpp
// Ciclo de vida
GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config = {});
void graph_renderer_destroy(GraphRenderer* r);
void graph_renderer_resize(GraphRenderer* r, int width, int height);
// Renderizado
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom);
```
## Ejemplo de uso con ImGui
```cpp
// Inicializacion (una vez)
GraphRenderer* renderer = graph_renderer_create(800, 600);
// En el render loop
ImVec2 panel_size = ImGui::GetContentRegionAvail();
graph_renderer_resize(renderer, (int)panel_size.x, (int)panel_size.y);
unsigned int tex = graph_renderer_draw(renderer, graph_data,
cam_x, cam_y, cam_zoom);
ImGui::Image((ImTextureID)(uintptr_t)tex,
panel_size,
ImVec2(0, 1), ImVec2(1, 0)); // flip Y para OpenGL
// Destruccion
graph_renderer_destroy(renderer);
```
## Notas de implementacion
**Renderizado de nodos:** Instanced rendering con un quad unitario [-0.5, 0.5] expandido por el tamano del nodo. El fragment shader aplica un SDF circular con anti-aliasing via `smoothstep` y un anillo de outline.
**Renderizado de aristas:** `GL_LINES` con datos de posicion y color empaquetados por arista. El ancho se controla con `GraphRendererConfig::edge_width`.
**Transformacion de camara:**
```
tx = -cam_x * zoom + width/2
ty = -cam_y * zoom + height/2
ndc = (screen / viewport) * 2 - 1
```
**Paleta de comunidades:** 10 colores ABGR usados cuando `node.color == 0`, seleccionados por `node.community % 10`.
**Estado GL:** Guarda y restaura `GL_FRAMEBUFFER_BINDING` y `GL_VIEWPORT` para ser compatible con el render loop de ImGui sin efectos secundarios.
**Includes GL:** Usa `#define GL_GLEXT_PROTOTYPES` + `<GL/gl.h>` + `<GL/glext.h>`. Si el proyecto carga funciones GL via glad/gl3w, reemplazar estos includes por el loader correspondiente.
+24
View File
@@ -0,0 +1,24 @@
#include "graph_types.h"
#include <cfloat>
void GraphData::update_bounds() {
if (node_count == 0) {
min_x = min_y = max_x = max_y = 0.0f;
return;
}
min_x = max_x = nodes[0].x;
min_y = max_y = nodes[0].y;
for (int i = 1; i < node_count; ++i) {
if (nodes[i].x < min_x) min_x = nodes[i].x;
if (nodes[i].x > max_x) max_x = nodes[i].x;
if (nodes[i].y < min_y) min_y = nodes[i].y;
if (nodes[i].y > max_y) max_y = nodes[i].y;
}
}
int GraphData::find_node(uint32_t id) const {
for (int i = 0; i < node_count; ++i) {
if (nodes[i].id == id) return i;
}
return -1;
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <cstdint>
// --- Graph node ---
struct GraphNode {
uint32_t id;
float x, y; // position in layout space
float vx, vy; // velocity (used by force layout)
float size; // visual radius (default 4.0)
uint32_t color; // ABGR packed (0 = use default palette)
const char* label; // optional display label (nullptr = none)
uint32_t community; // group/cluster ID (for auto-coloring)
float value; // arbitrary metric (for sizing)
bool pinned; // if true, force layout won't move this node
};
// --- Graph edge ---
struct GraphEdge {
uint32_t source; // index into GraphData::nodes
uint32_t target; // index into GraphData::nodes
float weight; // edge weight (affects attraction force)
uint32_t color; // ABGR packed (0 = default gray)
};
// --- Graph container ---
struct GraphData {
GraphNode* nodes;
int node_count;
GraphEdge* edges;
int edge_count;
// Bounding box (updated by layout)
float min_x, min_y, max_x, max_y;
// Recompute bounding box from node positions
void update_bounds();
// Find node index by id. Returns -1 if not found.
int find_node(uint32_t id) const;
};
// --- Helper: create a default node ---
inline GraphNode graph_node(uint32_t id, float x = 0, float y = 0) {
return {id, x, y, 0, 0, 4.0f, 0, nullptr, 0, 0, false};
}
// --- Helper: create an edge ---
inline GraphEdge graph_edge(uint32_t source, uint32_t target, float weight = 1.0f) {
return {source, target, weight, 0};
}
+327
View File
@@ -0,0 +1,327 @@
#include "viz/graph_viewport.h"
#include "viz/graph_types.h"
#include "viz/graph_renderer.h"
#include "viz/graph_force_layout.h"
#include "core/graph_spatial_hash.h"
#include "imgui.h"
#include <cstdio> // snprintf
#include <cstring> // memset
#include <vector>
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
static void viewport_to_graph(float vx, float vy,
float widget_x, float widget_y,
float widget_w, float widget_h,
float cam_x, float cam_y, float zoom,
float& gx, float& gy)
{
gx = (vx - widget_x - widget_w * 0.5f) / zoom + cam_x;
gy = (vy - widget_y - widget_h * 0.5f) / zoom + cam_y;
}
// ---------------------------------------------------------------------------
// graph_viewport_fit
// ---------------------------------------------------------------------------
void graph_viewport_fit(GraphData& graph, GraphViewportState& state)
{
graph.update_bounds();
if (graph.node_count == 0) {
state.cam_x = 0.0f;
state.cam_y = 0.0f;
state.zoom = 1.0f;
return;
}
float cx = (graph.min_x + graph.max_x) * 0.5f;
float cy = (graph.min_y + graph.max_y) * 0.5f;
state.cam_x = cx;
state.cam_y = cy;
float span_x = graph.max_x - graph.min_x;
float span_y = graph.max_y - graph.min_y;
float span = (span_x > span_y ? span_x : span_y);
// Use render dimensions if available; fall back to a safe default.
float view_px = (state.render_w > 0 ? (float)state.render_w : 600.0f);
float view_py = (state.render_h > 0 ? (float)state.render_h : 400.0f);
float view_min = (view_px < view_py ? view_px : view_py);
if (span > 0.0f) {
state.zoom = (view_min * 0.9f) / span;
} else {
state.zoom = 1.0f;
}
// Clamp to allowed range
if (state.zoom < state.zoom_min) state.zoom = state.zoom_min;
if (state.zoom > state.zoom_max) state.zoom = state.zoom_max;
}
// ---------------------------------------------------------------------------
// graph_viewport_destroy
// ---------------------------------------------------------------------------
void graph_viewport_destroy(GraphViewportState& state)
{
if (state.renderer) {
graph_renderer_destroy(state.renderer);
state.renderer = nullptr;
}
if (state.spatial) {
delete state.spatial;
state.spatial = nullptr;
}
state.initialized = false;
}
// ---------------------------------------------------------------------------
// graph_viewport — main widget
// ---------------------------------------------------------------------------
bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
ImVec2 size)
{
bool interacted = false;
// Resolve size
ImVec2 avail = ImGui::GetContentRegionAvail();
float w = (size.x > 0.0f) ? size.x : avail.x;
float h = (size.y > 0.0f) ? size.y : avail.y;
if (w < 1.0f) w = 1.0f;
if (h < 1.0f) h = 1.0f;
int iw = (int)w, ih = (int)h;
// -------------------------------------------------------------------
// 1. Lazy init
// -------------------------------------------------------------------
if (!state.initialized) {
state.renderer = graph_renderer_create(iw, ih);
state.spatial = new SpatialHash(20.0f, 4096);
state.render_w = iw;
state.render_h = ih;
state.initialized = true;
graph_viewport_fit(graph, state);
}
// -------------------------------------------------------------------
// 2. Resize
// -------------------------------------------------------------------
if (iw != state.render_w || ih != state.render_h) {
graph_renderer_resize(state.renderer, iw, ih);
state.render_w = iw;
state.render_h = ih;
}
// -------------------------------------------------------------------
// 3. Force layout step
// -------------------------------------------------------------------
if (state.layout_running && graph.node_count > 0) {
state.layout_energy = graph_force_layout_step(graph);
if (state.layout_energy < 0.01f) {
state.layout_running = false;
}
}
// -------------------------------------------------------------------
// 4. Build spatial hash
// -------------------------------------------------------------------
if (graph.node_count > 0) {
static std::vector<float> xs_buf, ys_buf, sz_buf;
xs_buf.resize(graph.node_count);
ys_buf.resize(graph.node_count);
sz_buf.resize(graph.node_count);
for (int i = 0; i < graph.node_count; ++i) {
xs_buf[i] = graph.nodes[i].x;
ys_buf[i] = graph.nodes[i].y;
sz_buf[i] = graph.nodes[i].size;
}
state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count);
}
// -------------------------------------------------------------------
// 5. Invisible button to capture input
// -------------------------------------------------------------------
ImGui::PushID(id);
ImVec2 widget_pos = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton("canvas", ImVec2(w, h),
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_MouseButtonMiddle|
ImGuiButtonFlags_MouseButtonRight);
bool hovered = ImGui::IsItemHovered();
bool lm_down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
bool lm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool mm_down = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
bool rm_down = ImGui::IsMouseDown(ImGuiMouseButton_Right);
ImVec2 mouse_pos = ImGui::GetMousePos();
float mx = mouse_pos.x, my = mouse_pos.y;
// Convert mouse to graph space
float gx_mouse, gy_mouse;
viewport_to_graph(mx, my,
widget_pos.x, widget_pos.y, w, h,
state.cam_x, state.cam_y, state.zoom,
gx_mouse, gy_mouse);
// -------------------------------------------------------------------
// 5a. Pan (middle or right mouse drag)
// -------------------------------------------------------------------
if (hovered && (mm_down || rm_down)) {
ImVec2 delta = ImGui::GetIO().MouseDelta;
if (delta.x != 0.0f || delta.y != 0.0f) {
state.cam_x -= delta.x / state.zoom;
state.cam_y -= delta.y / state.zoom;
interacted = true;
}
}
// -------------------------------------------------------------------
// 5b. Zoom (scroll wheel)
// -------------------------------------------------------------------
if (hovered) {
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) {
float old_zoom = state.zoom;
float new_zoom = old_zoom * (1.0f + wheel * 0.1f);
if (new_zoom < state.zoom_min) new_zoom = state.zoom_min;
if (new_zoom > state.zoom_max) new_zoom = state.zoom_max;
// Zoom toward cursor: keep gx_mouse/gy_mouse fixed in graph space
float rel_x = (mx - widget_pos.x - w * 0.5f);
float rel_y = (my - widget_pos.y - h * 0.5f);
state.cam_x += rel_x / old_zoom - rel_x / new_zoom;
state.cam_y += rel_y / old_zoom - rel_y / new_zoom;
state.zoom = new_zoom;
interacted = true;
}
}
// -------------------------------------------------------------------
// 5c. Hover — query nearest node
// -------------------------------------------------------------------
state.hovered_node = -1;
if (hovered && graph.node_count > 0) {
float hit_radius = 10.0f / state.zoom;
int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius);
if (nearest >= 0) {
state.hovered_node = nearest;
interacted = true;
}
}
// -------------------------------------------------------------------
// 5d. Node drag (left mouse down on a node)
// -------------------------------------------------------------------
if (hovered && lm_down) {
if (state.drag_node == -1 && state.hovered_node >= 0) {
state.drag_node = state.hovered_node;
state.is_dragging = true;
}
} else {
// Release drag
if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
graph.nodes[state.drag_node].pinned = false;
}
state.drag_node = -1;
state.is_dragging = false;
}
if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
GraphNode& n = graph.nodes[state.drag_node];
n.x = gx_mouse;
n.y = gy_mouse;
n.vx = 0.0f;
n.vy = 0.0f;
n.pinned = true;
interacted = true;
}
// -------------------------------------------------------------------
// 5e. Click — select node
// -------------------------------------------------------------------
if (hovered && lm_click && state.drag_node == -1) {
state.selected_node = state.hovered_node;
interacted = true;
}
// -------------------------------------------------------------------
// 5f. Keyboard shortcuts (only when widget is active/hovered)
// -------------------------------------------------------------------
if (hovered) {
if (ImGui::IsKeyPressed(ImGuiKey_Space)) {
state.layout_running = !state.layout_running;
}
if (ImGui::IsKeyPressed(ImGuiKey_F)) {
graph_viewport_fit(graph, state);
interacted = true;
}
}
// -------------------------------------------------------------------
// 6. Render to GPU texture
// -------------------------------------------------------------------
unsigned int tex_id = graph_renderer_draw(state.renderer, graph,
state.cam_x, state.cam_y,
state.zoom);
// -------------------------------------------------------------------
// 7. Display texture (flip UV for OpenGL FBO convention)
// -------------------------------------------------------------------
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddImage(
(ImTextureID)(intptr_t)tex_id,
widget_pos,
ImVec2(widget_pos.x + w, widget_pos.y + h),
ImVec2(0.0f, 1.0f), // UV top-left (flipped Y)
ImVec2(1.0f, 0.0f) // UV bottom-right
);
// -------------------------------------------------------------------
// 8. Tooltip on hovered node
// -------------------------------------------------------------------
if (state.hovered_node >= 0 && state.hovered_node < graph.node_count) {
const GraphNode& n = graph.nodes[state.hovered_node];
// Count degree
int degree = 0;
for (int i = 0; i < graph.edge_count; ++i) {
if ((int)graph.edges[i].source == state.hovered_node ||
(int)graph.edges[i].target == state.hovered_node) {
++degree;
}
}
ImGui::BeginTooltip();
if (n.label) ImGui::TextUnformatted(n.label);
ImGui::Text("ID: %u", n.id);
ImGui::Text("Community: %u", n.community);
ImGui::Text("Degree: %d", degree);
ImGui::Text("Value: %.3f", n.value);
ImGui::EndTooltip();
}
// -------------------------------------------------------------------
// 9. Status bar overlay
// -------------------------------------------------------------------
{
char status[128];
snprintf(status, sizeof(status),
"Nodes: %d | Edges: %d | Zoom: %.2fx | Energy: %.4f | [Space] layout [F] fit",
graph.node_count, graph.edge_count,
state.zoom, state.layout_energy);
ImVec2 text_pos = ImVec2(widget_pos.x + 6.0f, widget_pos.y + h - 18.0f);
draw_list->AddText(text_pos, IM_COL32(180, 180, 180, 200), status);
}
ImGui::PopID();
return interacted;
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include "imgui.h"
struct GraphData;
struct GraphRenderer;
struct SpatialHash;
// Persistent state for graph_viewport widget. Create one per viewport and keep
// alive across frames.
struct GraphViewportState {
// Camera
float cam_x = 0.0f, cam_y = 0.0f;
float zoom = 1.0f;
float zoom_min = 0.01f, zoom_max = 50.0f;
// Interaction result (read after calling graph_viewport each frame)
int hovered_node = -1; // node index under cursor, -1 if none
int selected_node = -1; // last clicked node index, -1 if none
bool is_dragging = false;
// Layout
bool layout_running = true; // animate force layout each frame
float layout_energy = 0.0f; // kinetic energy from last step
// Internal — managed by graph_viewport / graph_viewport_destroy
GraphRenderer* renderer = nullptr;
SpatialHash* spatial = nullptr;
bool initialized = false;
// Widget pixel dimensions tracked for resize detection
int render_w = 0, render_h = 0;
// Node being dragged (-1 = none)
int drag_node = -1;
};
// Main viewport widget. Call every ImGui frame.
// id: unique ImGui widget identifier
// graph: mutable graph data (node positions updated on drag)
// state: persistent state (camera, selection, GPU renderer); must outlive frames
// size: widget size in pixels — ImVec2(0,0) uses all available space
// Returns true if any user interaction occurred (hover, click, drag, zoom).
bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
ImVec2 size = ImVec2(0.0f, 0.0f));
// Release GPU resources. Call once when done with the viewport.
void graph_viewport_destroy(GraphViewportState& state);
// Fit camera to current graph bounds with 10% padding.
void graph_viewport_fit(GraphData& graph, GraphViewportState& state);
+119
View File
@@ -0,0 +1,119 @@
---
name: graph_viewport
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: impure
signature: "bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size)"
description: "Widget ImGui completo para visualizacion interactiva de grafos con pan, zoom, hover, seleccion y layout en vivo"
tags: [graph, viewport, imgui, interactive, pan, zoom, dashboard]
uses_functions: ["graph_renderer_cpp_viz", "graph_force_layout_cpp_viz", "graph_spatial_hash_cpp_core"]
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/graph_viewport.cpp"
framework: imgui
props:
- name: id
type: "const char*"
required: true
description: "Identificador unico del widget ImGui"
- name: graph
type: "GraphData&"
required: true
description: "Referencia al grafo (lectura de datos, escritura de posiciones al drag)"
- name: state
type: "GraphViewportState&"
required: true
description: "Estado persistente del viewport (camera, seleccion, renderer). Debe vivir mas que los frames."
- name: size
type: "ImVec2"
required: false
description: "Tamanio del widget en pixeles. ImVec2(0,0) usa todo el espacio disponible."
emits: []
has_state: true
params:
- name: id
desc: "Identificador unico del widget ImGui. Debe ser estable entre frames."
- name: graph
desc: "Grafo a visualizar. Las posiciones de nodos se modifican al arrastrar."
- name: state
desc: "Estado persistente: camara (cam_x, cam_y, zoom), nodo seleccionado/hovereado, renderer GPU, spatial hash. Alojado por el caller."
- name: size
desc: "Tamanio del widget en pixeles. (0,0) ocupa todo el espacio disponible en la ventana ImGui."
output: "true si hubo alguna interaccion del usuario en el frame actual (hover, click, drag, zoom, teclado)"
---
# graph_viewport
Widget ImGui self-contained para visualizar grafos interactivos. Integra rendering GPU, force-directed layout y hit-testing espacial en una sola llamada por frame.
## Uso basico
```cpp
// Declarar estado persistente (fuera del loop de render)
GraphViewportState vp_state;
// En el loop de render (dentro de una ventana ImGui):
if (graph_viewport("mi_grafo", my_graph, vp_state)) {
// hubo interaccion este frame
if (vp_state.selected_node >= 0) {
auto& n = my_graph.nodes[vp_state.selected_node];
// mostrar panel de detalle de n
}
}
// Al terminar:
graph_viewport_destroy(vp_state);
```
## Estado de camara
La camara usa coordenadas del espacio del grafo:
- `cam_x`, `cam_y`: centro de la camara en espacio del grafo
- `zoom`: pixeles por unidad de grafo
`graph_viewport_fit()` centra y ajusta el zoom para que el grafo quepa con 10% de padding.
## Controles
| Accion | Control |
|--------|---------|
| Pan | Boton medio o derecho + arrastrar |
| Zoom | Rueda del raton (hacia el cursor) |
| Seleccionar nodo | Click izquierdo |
| Arrastrar nodo | Click izquierdo sobre nodo |
| Toggle layout | Barra espaciadora |
| Fit camara | F |
## Force layout
El layout se ejecuta automaticamente cada frame mientras `state.layout_running == true`. Se detiene solo cuando la energia cinetica cae por debajo de `0.01`. Se puede pausar/reanudar con la barra espaciadora.
Los nodos arrastrados se marcan como `pinned = true` durante el drag, impidiendo que el force layout los mueva. Al soltar, `pinned` vuelve a `false`.
## Tooltip
Al hacer hover sobre un nodo se muestra un tooltip con: label, id numerico, community, degree (aristas conectadas) y value.
## Status bar
En la parte inferior del widget aparece: numero de nodos, aristas, zoom actual, energia del layout y recordatorio de atajos de teclado.
## Inicializacion lazy
El renderer OpenGL y el spatial hash se crean en el primer frame. La camara se ajusta automaticamente con `graph_viewport_fit` en la inicializacion.
## Notas de implementacion
- Usa `ImGui::InvisibleButton` con flags para los tres botones del raton, capturando input sin dibujar ningun boton visible.
- La textura del renderer se muestra con UV volteado en Y (`ImVec2(0,1)` a `ImVec2(1,0)`) para corregir la convencion de coordenadas de OpenGL vs ImGui.
- El spatial hash se reconstruye cada frame desde las posiciones actuales de los nodos, garantizando hit-testing correcto despues de drag o layout.
- El zoom hacia el cursor mantiene el punto del grafo bajo el cursor fijo en pantalla ajustando `cam_x`/`cam_y`.
+18
View File
@@ -0,0 +1,18 @@
#include "viz/histogram.h"
#include "implot.h"
void histogram(const char* title, const float* values, int count, int bins) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
int b = (bins > 0) ? bins : ImPlotBin_Sturges;
ImPlot::PlotHistogram("##data", values, count, b);
ImPlot::EndPlot();
}
}
void histogram(const char* title, const double* values, int count, int bins) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
int b = (bins > 0) ? bins : ImPlotBin_Sturges;
ImPlot::PlotHistogram("##data", values, count, b);
ImPlot::EndPlot();
}
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
// Renders a histogram using ImPlot::PlotHistogram.
// Call within an ImGui frame.
// bins == -1: automatic bin count via Sturges' rule.
void histogram(const char* title, const float* values, int count, int bins = -1);
void histogram(const char* title, const double* values, int count, int bins = -1);
+42
View File
@@ -0,0 +1,42 @@
---
name: histogram
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void histogram(const char* title, const float* values, int count, int bins = -1)"
description: "Renderiza un histograma con bins automaticos o manuales usando ImPlot PlotHistogram dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, histogram, distribution]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/histogram.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del histograma mostrado como cabecera del plot"
- name: values
desc: "Array de valores numericos a distribuir en bins"
- name: count
desc: "Numero de valores en el array"
- name: bins
desc: "Numero de bins. -1 = automatico via regla de Sturges (ImPlotBin_Sturges). Positivo = numero explicito de bins"
output: "Renderiza el histograma en el frame ImGui actual"
---
# histogram
Wrapper atomico sobre `ImPlot::PlotHistogram` con seleccion automatica del numero de bins.
Cuando `bins == -1` usa `ImPlotBin_Sturges`, que calcula el numero de bins como `ceil(log2(n)) + 1`. Para distribuciones con muchos valores o alta varianza puede preferirse pasar un valor explicito.
El plot usa `ImVec2(-1, 0)` para ocupar el ancho disponible con altura automatica.
Debe llamarse dentro del render callback de `fn::run_app`.
+44
View File
@@ -0,0 +1,44 @@
#include "kpi_card.h"
#include "sparkline.h"
#include <imgui.h>
#include <cstdio>
void kpi_card(const char* label, float value, float delta_percent,
const float* history, int history_count,
const char* format) {
ImGui::BeginGroup();
// Label — small, muted
ImGui::TextDisabled("%s", label);
// Value — scaled up font
ImGui::SetWindowFontScale(1.8f);
char value_buf[64];
snprintf(value_buf, sizeof(value_buf), format, value);
ImGui::Text("%s", value_buf);
ImGui::SetWindowFontScale(1.0f);
// Delta badge — green up arrow / red down arrow
const bool positive = delta_percent >= 0.0f;
const ImVec4 delta_color = positive
? ImVec4(0.20f, 0.80f, 0.35f, 1.0f) // green
: ImVec4(0.90f, 0.25f, 0.25f, 1.0f); // red
char delta_buf[32];
if (positive) {
snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xb2 +%.1f%%", delta_percent);
} else {
snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xbc %.1f%%", delta_percent);
}
ImGui::PushStyleColor(ImGuiCol_Text, delta_color);
ImGui::Text("%s", delta_buf);
ImGui::PopStyleColor();
// Sparkline — matches delta color
if (history != nullptr && history_count > 0) {
sparkline(label, history, history_count, delta_color, 120.0f, 24.0f);
}
ImGui::EndGroup();
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
// KPI card — displays a key metric with trend.
// Usage:
// float history[] = {10, 12, 11, 15, 18, 17, 20};
// kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f");
//
// Shows:
// - Label (small, muted)
// - Value (large font)
// - Delta badge (green up / red down)
// - Sparkline of history
void kpi_card(const char* label, float value, float delta_percent,
const float* history = nullptr, int history_count = 0,
const char* format = "%.1f");
+71
View File
@@ -0,0 +1,71 @@
---
name: kpi_card
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void kpi_card(const char* label, float value, float delta_percent, const float* history = nullptr, int history_count = 0, const char* format = \"%.1f\")"
description: "Card de KPI con valor grande, delta porcentual, y sparkline historico para dashboards"
tags: [imgui, kpi, card, dashboard, metrics, sparkline]
uses_functions: ["sparkline_cpp_viz"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/kpi_card.cpp"
framework: imgui
params:
- name: label
desc: "Nombre del KPI mostrado como header muted (ej: \"Revenue\", \"Users\")"
- name: value
desc: "Valor numerico actual del KPI"
- name: delta_percent
desc: "Cambio porcentual respecto al periodo anterior (positivo = mejora, negativo = deterioro)"
- name: history
desc: "Array de valores historicos para el sparkline. Nullable — si es nullptr no se renderiza sparkline"
- name: history_count
desc: "Numero de valores en el array history"
- name: format
desc: "Formato printf para el valor principal (ej: \"$%.0f\", \"%.1f%%\", \"%.2f\")"
output: "Renderiza la card KPI completa en el frame ImGui actual: label muted, valor grande, badge delta verde/rojo con triangulo, y sparkline de 120x24px"
---
# kpi_card
Card compacta para dashboards ImGui que muestra un KPI con contexto de tendencia. Combina label, valor escalado, badge de delta colorizado y sparkline historico en un grupo coherente de ~150px de ancho.
Usa `sparkline` del registry para el historico, con el mismo color que el badge (verde si delta >= 0, rojo si delta < 0).
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
float history[] = {10.0f, 12.0f, 11.0f, 15.0f, 18.0f, 17.0f, 20.0f};
kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f");
// Sin sparkline
kpi_card("Error Rate", 0.3f, -15.2f, nullptr, 0, "%.2f%%");
// Grid de KPIs
ImGui::Columns(3, "kpis", false);
kpi_card("MAU", 1250000.0f, 3.4f, mau_history, 30);
ImGui::NextColumn();
kpi_card("Revenue", 89400.0f, -1.2f, rev_history, 30, "$%.0f");
ImGui::NextColumn();
kpi_card("Churn", 2.1f, -0.3f, churn_history, 30, "%.1f%%");
ImGui::Columns(1);
```
## Notas
- El ancho total del grupo es aproximadamente 150px, apto para grids de 2-4 columnas.
- El escalado de fuente usa `SetWindowFontScale(1.8f)` — compatible con cualquier fuente cargada; no requiere fonts adicionales.
- Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) requieren que la fuente ImGui tenga el rango de simbolos geometricos cargado, o bien sustituir por ASCII (`^` y `v`).
- El color verde del delta es `ImVec4(0.20, 0.80, 0.35, 1.0)` y el rojo `ImVec4(0.90, 0.25, 0.25, 1.0)`, coherentes con los colores del sparkline subyacente.
- `BeginGroup`/`EndGroup` permite usar `SameLine()` despues de `kpi_card` y que el cursor avance correctamente.
+31
View File
@@ -0,0 +1,31 @@
#include "viz/pie_chart.h"
#include "implot.h"
void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::SetupAxesLimits(0, 1, 0, 1);
if (radius < 0.0f) {
// Donut mode: outer = |radius|, inner = 0.2
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, -radius, "%.1f", 90.0, ImPlotPieChartFlags_None);
} else {
float r = (radius > 0.0f) ? radius : 0.4f;
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0, ImPlotPieChartFlags_None);
}
ImPlot::EndPlot();
}
}
void pie_chart(const char* title, const char* const* labels, const double* values, int count, double radius) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::SetupAxesLimits(0, 1, 0, 1);
if (radius < 0.0) {
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, -radius, "%.1f", 90.0, ImPlotPieChartFlags_None);
} else {
double r = (radius > 0.0) ? radius : 0.4;
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0, ImPlotPieChartFlags_None);
}
ImPlot::EndPlot();
}
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
// Renders a pie or donut chart using ImPlot::PlotPieChart.
// Call within an ImGui frame.
// radius == 0: auto (0.4). radius > 0: explicit radius. radius < 0: donut mode (|radius| as outer, 0.2 as inner).
void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f);
void pie_chart(const char* title, const char* const* labels, const double* values, int count, double radius = 0.0);
+46
View File
@@ -0,0 +1,46 @@
---
name: pie_chart
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f)"
description: "Renderiza un grafico circular (pie/donut) usando ImPlot PlotPieChart dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, pie, donut]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/pie_chart.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del grafico"
- name: labels
desc: "Array de etiquetas para cada segmento del pie"
- name: values
desc: "Array de valores numericos para cada segmento"
- name: count
desc: "Numero de segmentos (longitud de labels y values)"
- name: radius
desc: "Radio del pie (0 = auto 0.4). Positivo = radio explicito. Negativo = modo donut con outer radius = |radius| e inner = 0.2"
output: "Renderiza el grafico circular en el frame ImGui actual"
---
# pie_chart
Wrapper atomico sobre `ImPlot::PlotPieChart` con soporte para modo pie y modo donut.
El eje del plot se configura con `ImPlotAxisFlags_NoDecorations` para ocultar los ejes y mostrar solo el grafico circular. El aspecto se fuerza a cuadrado con `ImPlotFlags_Equal`.
**Modo pie** (`radius >= 0`): dibuja un pie chart solido. Si `radius == 0`, usa radio automatico de 0.4.
**Modo donut** (`radius < 0`): usa `|radius|` como radio exterior. El agujero interior es fijo en 0.2, suficiente para texto central.
Debe llamarse dentro del render callback de `fn::run_app`.
+76
View File
@@ -0,0 +1,76 @@
#include "viz/sparkline.h"
#include "imgui.h"
void sparkline(const char* id, const float* values, int count, ImVec4 color,
float width, float height) {
if (count <= 0) return;
ImGui::PushID(id);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImDrawList* draw_list = ImGui::GetWindowDrawList();
// Reserve inline space
ImGui::Dummy(ImVec2(width, height));
// Find min/max for Y auto-scale
float min_val = values[0];
float max_val = values[0];
for (int i = 1; i < count; i++) {
if (values[i] < min_val) min_val = values[i];
if (values[i] > max_val) max_val = values[i];
}
float range = max_val - min_val;
if (range < 1e-6f) range = 1.0f; // avoid division by zero for flat lines
// Fill area under curve (low alpha)
if (count >= 2) {
ImU32 fill_color = IM_COL32(
(int)(color.x * 255),
(int)(color.y * 255),
(int)(color.z * 255),
40);
// Build fill polygon: baseline bottom-left -> points -> baseline bottom-right
// We use AddConvexPolyFilled workaround: draw as a series of triangles from baseline
float x0 = pos.x;
float y_base = pos.y + height;
for (int i = 0; i + 1 < count; i++) {
float xa = x0 + ((float)i / (count - 1)) * width;
float xb = x0 + ((float)(i + 1) / (count - 1)) * width;
float ya = pos.y + height - ((values[i] - min_val) / range) * height;
float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height;
draw_list->AddQuadFilled(
ImVec2(xa, y_base),
ImVec2(xa, ya),
ImVec2(xb, yb),
ImVec2(xb, y_base),
fill_color);
}
}
// Draw polyline
ImU32 line_color = IM_COL32(
(int)(color.x * 255),
(int)(color.y * 255),
(int)(color.z * 255),
(int)(color.w * 255));
for (int i = 0; i + 1 < count; i++) {
float xa = pos.x + ((float)i / (count - 1)) * width;
float xb = pos.x + ((float)(i + 1) / (count - 1)) * width;
float ya = pos.y + height - ((values[i] - min_val) / range) * height;
float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height;
draw_list->AddLine(ImVec2(xa, ya), ImVec2(xb, yb), line_color, 1.5f);
}
ImGui::PopID();
}
void sparkline(const char* id, const float* values, int count,
float width, float height) {
// Default color: soft green
sparkline(id, values, count, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), width, height);
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "imgui.h"
// Renders a mini inline line chart for use in tables, headers and KPI cards.
// Auto-scales Y to the min/max of values.
// Uses PushID/PopID with id for uniqueness inside tables.
void sparkline(const char* id, const float* values, int count,
float width = 100.0f, float height = 20.0f);
void sparkline(const char* id, const float* values, int count, ImVec4 color,
float width = 100.0f, float height = 20.0f);
+69
View File
@@ -0,0 +1,69 @@
---
name: sparkline
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void sparkline(const char* id, const float* values, int count, float width = 100.0f, float height = 20.0f)"
description: "Renderiza un mini grafico de lineas inline para uso en tablas, headers y KPI cards"
tags: [imgui, visualization, sparkline, inline, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/sparkline.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico del widget, usado con PushID/PopID para garantizar unicidad en tablas"
- name: values
desc: "Array de valores float del sparkline (serie temporal)"
- name: count
desc: "Numero de valores en el array"
- name: width
desc: "Ancho en pixels del sparkline (default 100.0)"
- name: height
desc: "Alto en pixels del sparkline (default 20.0)"
output: "Renderiza el sparkline inline en el frame ImGui actual, reservando espacio con ImGui::Dummy"
---
# sparkline
Mini grafico de lineas inline construido sobre ImGui draw primitives. No requiere ImPlot.
Auto-escala el eje Y al rango minimo/maximo de los valores. Dibuja una polyline con relleno semitransparente bajo la curva. Disenado para encajar en celdas de tablas, headers y tarjetas KPI.
Ofrece dos overloads:
- Sin color: usa verde suave por defecto (`ImVec4(0.35, 0.85, 0.45, 1.0)`)
- Con color: acepta cualquier `ImVec4` para personalizar la linea y el relleno
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
// En una celda de tabla
ImGui::TableNextColumn();
sparkline("##revenue_spark", revenue.data(), (int)revenue.size(), 80.0f, 18.0f);
// Con color personalizado (rojo para valores negativos)
sparkline("##pnl", pnl.data(), (int)pnl.size(),
ImVec4(0.9f, 0.3f, 0.3f, 1.0f), 100.0f, 20.0f);
// KPI card inline con label
ImGui::Text("Revenue"); ImGui::SameLine();
sparkline("kpi_rev", data, count);
```
## Notas
- El relleno bajo la curva usa alpha 40/255 del mismo color de la linea.
- Si todos los valores son iguales (rango < 1e-6), la linea se dibuja en el centro verticalmente.
- El grosor de linea es 1.5px para que sea legible a alturas de 16-24px.
- `id` no se muestra visualmente; solo se pasa a `PushID` para que ImGui diferencie widgets con los mismos datos en la misma tabla.
+12
View File
@@ -0,0 +1,12 @@
#include "viz/surface_plot_3d.h"
#include "imgui.h"
void surface_plot_3d(const char* title, const float* values, int rows, int cols,
float z_min, float z_max) {
ImGui::BeginGroup();
ImGui::TextDisabled("[STUB] %s", title);
ImGui::TextWrapped("surface_plot_3d requires ImPlot3D. "
"Add it to cpp/vendor/implot3d/ and rebuild.");
ImGui::Text("Data: %dx%d, range [%.2f, %.2f]", rows, cols, z_min, z_max);
ImGui::EndGroup();
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
// [STUB] Renders a 3D surface plot using ImPlot3D.
// Requires ImPlot3D to be vendored in cpp/vendor/implot3d/.
// Until then, displays a placeholder message inside an ImGui group.
// Call within an ImGui frame (inside fn::run_app render callback).
void surface_plot_3d(const char* title, const float* values, int rows, int cols,
float z_min, float z_max);
+61
View File
@@ -0,0 +1,61 @@
---
name: surface_plot_3d
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void surface_plot_3d(const char* title, const float* values, int rows, int cols, float z_min, float z_max)"
description: "[STUB] Renderiza una superficie 3D — requiere ImPlot3D (no vendoreado aun)"
tags: [implot3d, chart, visualization, gpu, surface, 3d, stub]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/surface_plot_3d.cpp"
framework: imgui
params:
- name: title
desc: "Titulo de la superficie, se muestra como header del plot"
- name: values
desc: "Array row-major de alturas Z con dimension rows*cols"
- name: rows
desc: "Numero de filas de la grilla de la superficie"
- name: cols
desc: "Numero de columnas de la grilla de la superficie"
- name: z_min
desc: "Valor minimo del eje Z para escalar el colormap"
- name: z_max
desc: "Valor maximo del eje Z para escalar el colormap"
output: "Renderiza un placeholder informativo en el frame ImGui actual; cuando ImPlot3D este disponible, renderizara la superficie 3D"
---
# surface_plot_3d
**STUB** — la implementacion real requiere [ImPlot3D](https://github.com/brenocq/implot3d), que todavia no esta vendoreado en `cpp/vendor/implot3d/`.
Mientras tanto la funcion renderiza un grupo ImGui con un mensaje informativo que muestra el titulo, las dimensiones de la grilla y el rango Z. La firma es definitiva y no cambiara cuando se integre ImPlot3D.
## Dependencia pendiente
Para activar la implementacion real:
1. Clonar o copiar ImPlot3D en `cpp/vendor/implot3d/`
2. Anadir `implot3d.cpp` al build system (CMake / Makefile)
3. Reemplazar el cuerpo de `surface_plot_3d` por la llamada a `ImPlot3D::BeginPlot3D` / `ImPlot3D::PlotSurface` / `ImPlot3D::EndPlot3D`
4. Actualizar `imports` del .md a `[imgui, implot3d]` y quitar el tag `stub`
## Uso
```cpp
// values es un array row-major de rows*cols floats
float grid[4 * 4] = { ... };
surface_plot_3d("Mi Superficie", grid, 4, 4, -1.0f, 1.0f);
```
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
+32
View File
@@ -0,0 +1,32 @@
#include "viz/table_view.h"
#include "imgui.h"
bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count) {
ImGuiTableFlags flags =
ImGuiTableFlags_Borders |
ImGuiTableFlags_Sortable |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable |
ImGuiTableFlags_ScrollY |
ImGuiTableFlags_Reorderable;
if (!ImGui::BeginTable(id, col_count, flags, ImVec2(0, 300))) {
return false;
}
for (int col = 0; col < col_count; col++) {
ImGui::TableSetupColumn(headers[col]);
}
ImGui::TableHeadersRow();
for (int row = 0; row < row_count; row++) {
ImGui::TableNextRow();
for (int col = 0; col < col_count; col++) {
ImGui::TableSetColumnIndex(col);
ImGui::TextUnformatted(cells[row * col_count + col]);
}
}
ImGui::EndTable();
return true;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Renders an interactive table with sorting indicators and scroll using the ImGui Tables API.
// Call within an ImGui frame.
// Returns true if the table was rendered visible, false if clipped/skipped.
bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count);
+67
View File
@@ -0,0 +1,67 @@
---
name: table_view
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count)"
description: "Renderiza una tabla interactiva con sorting y scroll usando ImGui Tables API"
tags: [imgui, table, visualization, dashboard, data]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/table_view.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico de la tabla para ImGui (debe ser unico en el frame)"
- name: headers
desc: "Array de strings con los nombres de las columnas"
- name: col_count
desc: "Numero de columnas"
- name: cells
desc: "Array flat row-major de strings; acceso a celda (row, col) via cells[row * col_count + col]"
- name: row_count
desc: "Numero de filas de datos, sin contar el header"
output: "true si la tabla se renderizo visible, false si fue clipped o skipped por ImGui"
---
# table_view
Wrapper atomico sobre `ImGui::BeginTable` / `ImGui::EndTable`. Renderiza una tabla con las siguientes capacidades:
- **Borders**: bordes entre celdas y columnas
- **Sortable**: muestra indicadores de orden en los headers (el caller es responsable de ordenar `cells` antes de llamar)
- **RowBg**: filas alternadas con color de fondo
- **Resizable**: el usuario puede arrastrar los separadores de columna
- **ScrollY**: scroll vertical con altura fija de 300px
- **Reorderable**: el usuario puede reordenar columnas arrastrando los headers
El caller controla el orden de los datos — `table_view` solo habilita el flag `Sortable` para que ImGui muestre los indicadores visuales, pero no reordena `cells` internamente.
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
const char* headers[] = {"Nombre", "Valor", "Estado"};
const char* cells[] = {
"Alpha", "1.23", "OK",
"Beta", "4.56", "WARN",
"Gamma", "7.89", "ERROR",
};
table_view("##mi_tabla", headers, 3, cells, 3);
```
## Notas
- `id` debe comenzar con `##` si no se quiere mostrar como titulo de ventana en el contexto ImGui.
- El outer size fijo de `ImVec2(0, 300)` puede parametrizarse en una version futura.
- El sorting real de datos queda fuera del scope de esta funcion para mantenerla pura y componible.
+106
View File
@@ -0,0 +1,106 @@
---
name: GraphData
lang: cpp
domain: viz
version: "1.0.0"
algebraic: product
definition: |
struct GraphNode {
uint32_t id;
float x, y;
float vx, vy;
float size;
uint32_t color;
const char* label;
uint32_t community;
float value;
bool pinned;
};
struct GraphEdge {
uint32_t source;
uint32_t target;
float weight;
uint32_t color;
};
struct GraphData {
GraphNode* nodes;
int node_count;
GraphEdge* edges;
int edge_count;
float min_x, min_y, max_x, max_y;
void update_bounds();
int find_node(uint32_t id) const;
};
description: "Tipos de datos base para el sistema de grafos GPU del registry. GraphNode modela un vertice con posicion, velocidad, apariencia y metadatos de layout. GraphEdge modela una arista con peso y color. GraphData es el contenedor principal que agrupa nodos y aristas con bounding box y metodos de consulta. Disenado para integrarse con force-directed layout y renderizado GPU via ImGui/ImPlot."
tags: [graph, network, visualization, gpu, force-layout, node, edge, imgui]
uses_types: []
file_path: "cpp/functions/viz/graph_types.h"
---
## Structs
### GraphNode
Vertice del grafo. Contiene todos los datos necesarios para el layout y el renderizado.
| Campo | Tipo | Descripcion |
|---|---|---|
| `id` | `uint32_t` | Identificador unico del nodo |
| `x, y` | `float` | Posicion en el espacio de layout |
| `vx, vy` | `float` | Velocidad del nodo, usada por el algoritmo force-directed |
| `size` | `float` | Radio visual en pixels (por defecto 4.0) |
| `color` | `uint32_t` | Color en formato ABGR packed. 0 = usar paleta automatica por community |
| `label` | `const char*` | Etiqueta visible. `nullptr` = sin etiqueta |
| `community` | `uint32_t` | ID de grupo/cluster para auto-coloreo. 0 = sin grupo |
| `value` | `float` | Metrica arbitraria (puede usarse para escalar el tamaño del nodo) |
| `pinned` | `bool` | Si es `true`, el force layout no mueve este nodo |
### GraphEdge
Arista del grafo. Referencia nodos por indice (no por id) para acceso O(1) en el loop de simulacion.
| Campo | Tipo | Descripcion |
|---|---|---|
| `source` | `uint32_t` | Indice en `GraphData::nodes` del nodo origen |
| `target` | `uint32_t` | Indice en `GraphData::nodes` del nodo destino |
| `weight` | `float` | Peso de la arista. Afecta la fuerza de atraccion en el layout |
| `color` | `uint32_t` | Color ABGR packed. 0 = gris por defecto |
### GraphData
Contenedor principal. Posee los arrays de nodos y aristas (memoria gestionada externamente). Mantiene un bounding box actualizable para proyeccion de coordenadas a pantalla.
| Campo/Metodo | Descripcion |
|---|---|
| `nodes` / `node_count` | Array de nodos y su longitud |
| `edges` / `edge_count` | Array de aristas y su longitud |
| `min_x, min_y, max_x, max_y` | Bounding box calculado sobre las posiciones actuales |
| `update_bounds()` | Recalcula el bounding box iterando todos los nodos |
| `find_node(id)` | Busqueda lineal por `GraphNode::id`. Retorna -1 si no existe |
## Helpers
```cpp
// Crear un nodo con valores por defecto
GraphNode n = graph_node(42, 100.0f, 200.0f);
// Crear una arista con peso por defecto 1.0
GraphEdge e = graph_edge(0, 1, 2.5f);
```
## Implementacion
Los metodos `update_bounds()` y `find_node()` estan implementados en `cpp/functions/viz/graph_types.cpp`.
`update_bounds()` es O(n) sobre `node_count`. Llamar despues de cada step del layout para mantener el bounding box fresco.
`find_node()` es O(n) por busqueda lineal. Para grafos grandes (>10k nodos) considerar mantener un `unordered_map<uint32_t, int>` externo como indice.
## Notas de diseño
- La memoria de `nodes` y `edges` es propiedad del caller. `GraphData` no hace `new`/`delete`.
- `color` usa formato ABGR packed (compatible con ImGui `ImU32`): `0xAABBGGRR`.
- Las aristas referencian por indice, no por id, para que el loop de simulacion sea cache-friendly.
- `community` con valor 0 se interpreta como "sin grupo" — los colores de comunidad empiezan desde 1.
@@ -102,4 +102,6 @@ Output siempre JSON. En error retorna `{"error": "..."}` por stderr con exit cod
- `jupyter_kernel_execute` es sincrona directamente porque `KernelClient.execute` es bloqueante.
- El token puede ser cadena vacia si el servidor tiene autenticacion deshabilitada.
- `NbModelClient` requiere que el servidor tenga habilitado el endpoint colaborativo (`/api/collaboration/`), disponible en JupyterLab >= 4 con `jupyter-collaboration` instalado.
- **Auto-init**: `jupyter_append_execute` crea el notebook automaticamente si no existe (via REST PUT /api/contents) y arranca una sesion con kernel si no hay ninguna activa para ese notebook (via POST /api/sessions). No es necesario abrir el notebook manualmente en el navegador.
- **Auto-session**: `jupyter_execute_cell` tambien garantiza que exista una sesion con kernel antes de ejecutar.
- **Fix Issue 006**: `jupyter_execute_cell` normaliza la celda antes de ejecutar. Las celdas creadas manualmente (no via la UI de Jupyter) pueden carecer de `outputs` o `execution_count` en el modelo CRDT, lo que causaba `KeyError: 'outputs'` dentro de `execute_cell` al hacer `del ycell["outputs"][:]`. El fix lee la celda con `nb[cell_index]`, detecta los campos faltantes, y reemplaza la celda via `nb[cell_index] = _normalize_code_cell(cell)` — que usa `set_cell` internamente para re-crear el mapa CRDT completo preservando el source original.
+80 -4
View File
@@ -10,7 +10,7 @@ import asyncio
import json
from functools import partial
from typing import Any
from urllib.error import URLError
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from jupyter_kernel_client import KernelClient
@@ -23,6 +23,80 @@ from nbformat import NotebookNode
# ---------------------------------------------------------------------------
def _notebook_exists(notebook_path: str, server_url: str, token: str) -> bool:
"""Comprueba si un notebook existe en el servidor Jupyter via HEAD /api/contents."""
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"token {token}"
check_url = f"{server_url}/api/contents/{notebook_path}"
req = Request(check_url, headers=headers, method="HEAD")
try:
with urlopen(req, timeout=5):
return True
except HTTPError as e:
if e.code == 404:
return False
raise
def _create_notebook(notebook_path: str, server_url: str, token: str, kernel_name: str = "python3") -> None:
"""Crea un notebook vacio via PUT /api/contents si no existe."""
if _notebook_exists(notebook_path, server_url, token):
return
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if token:
headers["Authorization"] = f"token {token}"
kernel_display = {"python3": "Python 3 (ipykernel)", "python": "Python 3"}.get(kernel_name, kernel_name)
notebook_content = {
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {
"kernelspec": {"name": kernel_name, "display_name": kernel_display, "language": "python"},
"language_info": {"name": "python"},
},
"cells": [],
}
body = json.dumps({"type": "notebook", "content": notebook_content}).encode("utf-8")
url = f"{server_url}/api/contents/{notebook_path}"
req = Request(url, data=body, headers=headers, method="PUT")
with urlopen(req, timeout=10) as resp:
resp.read()
def _ensure_session(server_url: str, token: str, notebook_path: str, kernel_name: str = "python3") -> str:
"""Garantiza que exista una sesion para el notebook. Retorna el kernel_id.
Si ya hay una sesion activa, retorna su kernel_id. Si no, crea una nueva
via POST /api/sessions (lo cual tambien arranca un kernel).
"""
kernel_id = _resolve_kernel_id(server_url, token, notebook_path)
if kernel_id:
return kernel_id
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
if token:
headers["Authorization"] = f"token {token}"
body = json.dumps({
"path": notebook_path,
"type": "notebook",
"kernel": {"name": kernel_name},
}).encode("utf-8")
url = f"{server_url}/api/sessions"
req = Request(url, data=body, headers=headers, method="POST")
with urlopen(req, timeout=10) as resp:
session = json.loads(resp.read())
return session.get("kernel", {}).get("id", "")
def _api_get(url: str, token: str = "") -> dict | list | None:
"""GET a Jupyter REST API endpoint."""
headers = {"Accept": "application/json"}
@@ -112,13 +186,14 @@ async def _async_append_execute(
server_url: str,
token: str,
) -> dict[str, Any]:
_create_notebook(notebook_path, server_url, token)
kernel_id = _ensure_session(server_url, token, notebook_path)
ws_url = get_jupyter_notebook_websocket_url(
server_url,
notebook_path,
token or None,
)
kernel_id = _resolve_kernel_id(server_url, token, notebook_path)
username = _resolve_collab_username(server_url, token)
async with NbModelClient(ws_url, username=username) as nb:
@@ -149,12 +224,13 @@ async def _async_execute_cell(
server_url: str,
token: str,
) -> dict[str, Any]:
kernel_id = _ensure_session(server_url, token, notebook_path)
ws_url = get_jupyter_notebook_websocket_url(
server_url,
notebook_path,
token or None,
)
kernel_id = _resolve_kernel_id(server_url, token, notebook_path)
username = _resolve_collab_username(server_url, token)
async with NbModelClient(ws_url, username=username) as nb:
+10 -2
View File
@@ -6,8 +6,8 @@ domain: notebook
version: "1.0.0"
purity: impure
signature: "def jupyter_kernel_list(server_url: str = \"http://localhost:8888\", token: str = \"\") -> list[dict]"
description: "CRUD completo de kernels Jupyter via REST API. Expone seis operaciones: list, start, restart, interrupt, shutdown y sessions. Usa solo stdlib (urllib, json), sin dependencias externas."
tags: [jupyter, notebook, kernel, api, http, rest, sessions, crud]
description: "CRUD completo de kernels Jupyter via REST API. Expone ocho operaciones: list, start, restart, interrupt, shutdown, sessions, cleanup y shutdown-all. Usa solo stdlib (urllib, json), sin dependencias externas."
tags: [jupyter, notebook, kernel, api, http, rest, sessions, crud, cleanup]
uses_functions: []
uses_types: []
returns: []
@@ -31,6 +31,8 @@ file_path: "python/functions/notebook/jupyter_kernel.py"
| `jupyter_kernel_interrupt(server_url, token, kernel_id)` | `POST /api/kernels/{id}/interrupt` | Interrumpe ejecucion |
| `jupyter_kernel_shutdown(server_url, token, kernel_id)` | `DELETE /api/kernels/{id}` | Apaga y elimina un kernel |
| `jupyter_kernel_sessions(server_url, token)` | `GET /api/sessions` | Lista sesiones activas |
| `jupyter_kernel_cleanup(server_url, token, idle_seconds)` | `GET + DELETE` | Apaga kernels inactivos |
| `jupyter_kernel_shutdown_all(server_url, token)` | `GET + DELETE` | Apaga todos los kernels |
## Ejemplo
@@ -88,6 +90,12 @@ python python/functions/notebook/jupyter_kernel.py shutdown abc123-...
# Listar sesiones
python python/functions/notebook/jupyter_kernel.py sessions
# Limpiar kernels inactivos (default: 1h sin actividad)
python python/functions/notebook/jupyter_kernel.py cleanup --idle-seconds 1800
# Apagar todos los kernels
python python/functions/notebook/jupyter_kernel.py shutdown-all
```
Todos los subcomandos aceptan `--server` y `--token`. El output es siempre JSON.
@@ -196,6 +196,80 @@ def jupyter_kernel_sessions(
return sessions
def jupyter_kernel_cleanup(
server_url: str = "http://localhost:8888",
token: str = "",
idle_seconds: int = 3600,
) -> list[dict]:
"""Apaga todos los kernels que llevan mas de idle_seconds sin actividad.
Util para liberar recursos en servidores con muchos notebooks abiertos.
Por defecto cierra kernels inactivos desde hace mas de 1 hora.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
idle_seconds: Segundos de inactividad para considerar un kernel ocioso.
Returns:
Lista de dicts con los kernels apagados (id, name, last_activity, idle_seconds).
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
from datetime import datetime, timezone
kernels = jupyter_kernel_list(server_url, token)
now = datetime.now(timezone.utc)
shutdown_list = []
for k in kernels:
last_activity = k.get("last_activity", "")
if not last_activity:
continue
try:
last_dt = datetime.fromisoformat(last_activity.replace("Z", "+00:00"))
idle = (now - last_dt).total_seconds()
except (ValueError, TypeError):
continue
if idle >= idle_seconds:
jupyter_kernel_shutdown(server_url, token, k["id"])
shutdown_list.append({
"id": k["id"],
"name": k.get("name", ""),
"last_activity": last_activity,
"idle_seconds": int(idle),
})
return shutdown_list
def jupyter_kernel_shutdown_all(
server_url: str = "http://localhost:8888",
token: str = "",
) -> list[dict]:
"""Apaga todos los kernels activos del servidor.
Args:
server_url: URL base del servidor Jupyter.
token: Token de autenticacion. Vacio si el servidor no requiere auth.
Returns:
Lista de dicts con los kernels apagados (id, name).
Raises:
urllib.error.HTTPError: Si la respuesta HTTP indica un error.
urllib.error.URLError: Si no se puede conectar al servidor.
"""
kernels = jupyter_kernel_list(server_url, token)
shutdown_list = []
for k in kernels:
jupyter_kernel_shutdown(server_url, token, k["id"])
shutdown_list.append({"id": k["id"], "name": k.get("name", "")})
return shutdown_list
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -248,6 +322,18 @@ if __name__ == "__main__":
# sessions
subparsers.add_parser("sessions", help="Lista las sesiones activas.")
# cleanup
sp_cleanup = subparsers.add_parser("cleanup", help="Apaga kernels inactivos.")
sp_cleanup.add_argument(
"--idle-seconds",
type=int,
default=3600,
help="Segundos de inactividad para considerar ocioso (default: 3600)",
)
# shutdown-all
subparsers.add_parser("shutdown-all", help="Apaga todos los kernels activos.")
args = parser.parse_args()
try:
@@ -267,6 +353,10 @@ if __name__ == "__main__":
result = {"status": "shutdown", "kernel_id": args.kernel_id}
elif args.command == "sessions":
result = jupyter_kernel_sessions(args.server, args.token)
elif args.command == "cleanup":
result = jupyter_kernel_cleanup(args.server, args.token, args.idle_seconds)
elif args.command == "shutdown-all":
result = jupyter_kernel_shutdown_all(args.server, args.token)
else:
parser.print_help()
sys.exit(1)
+2 -1
View File
@@ -153,4 +153,5 @@ python -m notebook.jupyter_write delete notebooks/01.ipynb 3
- NO ejecuta celdas — solo modifica la estructura. Para ejecutar, usar `jupyter_exec`.
- `server_url` y `token` tienen defaults convenientes para desarrollo local (`http://localhost:8888`, token vacio).
- El campo `cell_index` en el resultado refleja la posicion final de la celda en el notebook.
- Patron tipico: `create` para crear el notebook, luego `batch` para poblar las celdas iniciales.
- `append_code`, `append_markdown` y `batch` crean el notebook automaticamente si no existe (auto-create via REST). No es necesario llamar a `create` previamente.
- Patron tipico: `batch` para poblar las celdas iniciales (crea el notebook si no existe), o `create` + `batch` si se necesita control explicito.
@@ -30,6 +30,35 @@ def _resolve_collab_username(server_url: str, token: str) -> str:
return "Anonymous"
# ---------------------------------------------------------------------------
# Helpers internos
# ---------------------------------------------------------------------------
def _notebook_exists(notebook_path: str, server_url: str, token: str) -> bool:
"""Comprueba si un notebook existe en el servidor Jupyter via HEAD /api/contents."""
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"token {token}"
check_url = f"{server_url}/api/contents/{notebook_path}"
req = Request(check_url, headers=headers, method="HEAD")
try:
with urlopen(req, timeout=5):
return True
except HTTPError as e:
if e.code == 404:
return False
raise
def _auto_create_notebook(notebook_path: str, server_url: str, token: str) -> bool:
"""Crea el notebook si no existe. Retorna True si fue creado."""
if not _notebook_exists(notebook_path, server_url, token):
jupyter_create_notebook(notebook_path, server_url=server_url, token=token)
return True
return False
# ---------------------------------------------------------------------------
# Helpers internos async
# ---------------------------------------------------------------------------
@@ -42,6 +71,7 @@ async def _append_cell(
server_url: str,
token: str,
) -> dict:
_auto_create_notebook(notebook_path, server_url, token)
ws_url = get_jupyter_notebook_websocket_url(
server_url=server_url,
token=token,
@@ -139,6 +169,7 @@ async def _batch_write(
token: str,
) -> dict:
"""Anade multiples celdas en una sola conexion WebSocket."""
_auto_create_notebook(notebook_path, server_url, token)
ws_url = get_jupyter_notebook_websocket_url(
server_url=server_url,
token=token,