14 Commits

Author SHA1 Message Date
egutierrez 0bdb3d72d7 feat: add issue 0008 — SQLite API Web service
App Go que expone registry.db y operations.db de cada app como API REST
read-only en localhost:8484, para acceso programático sin SQLite directo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:27:54 +02:00
egutierrez 4e8bbb0a88 merge: quick/registry-dashboard-and-rules — Dashboard ImGui, viewport multi-ventana, reglas tags y issues DAG engine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:18:36 +02:00
egutierrez ffbcafa52d chore: update registry.db with fullscreen_window function and registry_dashboard app
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:18:22 +02:00
egutierrez d9b448a07b feat: add DAG engine issues (0007a-e) and feature flag
Desglose del sistema de orquestacion propio para reemplazar Dagu:
- 0007a: core puro (parse, validate, topo sort)
- 0007b: process manager (spawn, wait, kill)
- 0007c: execution store (SQLite)
- 0007d: scheduler (cron parser, ticker)
- 0007e: app CLI que compone todo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:18:19 +02:00
egutierrez 5c712bb974 docs: update /app and /create_functions skills with service tag and Gitea rules
- /app: Gitea publicacion obligatoria, tag service para daemons, flujo C++ e ImGui,
  prefijo service: para crear services directamente
- /create_functions: reglas de tags launcher y service en la seccion de reglas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:18:13 +02:00
egutierrez 29dee49a36 docs: rename tag_launcher to function_tags, add service tag convention
Renombra la regla y documenta el tag service para apps de larga duracion
(APIs, daemons, watchers). Un service es una app con tag service, no una
tipologia separada.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:18:06 +02:00
egutierrez f0d9ffa2bb chore: remove leftover sqlite3 tarball from vendor
Solo se necesitan sqlite3.c, sqlite3.h y sqlite3ext.h para la amalgamation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:18:00 +02:00
egutierrez 132a7d3240 feat: add multi-viewport support and SQLite amalgamation to C++ framework
- AppConfig.viewports flag para ventanas OS reales fuera del main window
- Multi-viewport render loop en app_base.cpp (UpdatePlatformWindows)
- SQLite amalgamation vendoreada para Windows cross-compile
- LANGUAGES C CXX en CMakeLists para compilar sqlite3.c
- Fix pie_chart.cpp para nueva API de ImPlot (PlotPieChart sin flags arg)
- imgui.ini añadido a gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:17:50 +02:00
egutierrez dcd1843609 feat: add fullscreen_window C++ function for ImGui apps
Componente que crea una ventana ImGui fullscreen sin decoraciones, eliminando
la necesidad de usar el sistema de ventanas interno. Usado por registry_dashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 01:17:44 +02:00
egutierrez d2ae672a23 merge: quick/cpp-notebook-commands — Funciones C++ ImGui, mejoras notebook, agentes Claude 2026-04-08 00:10:43 +02:00
egutierrez 76a607cf6f chore: update registry.db with C++ functions and notebook enhancements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:10:31 +02:00
egutierrez a1b7e5e143 chore: add claude agent definitions and command templates
Agentes especializados (fn-constructor, fn-executor, fn-recopilador)
y comandos de usuario (analysis, app, create_functions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:10:27 +02:00
egutierrez fc8062bade feat: enhance jupyter notebook functions with auto-init and kernel management
Auto-create notebooks y sesiones en jupyter_exec (append y cell).
Auto-create en jupyter_write (append_code, append_markdown, batch).
Nuevos subcomandos cleanup y shutdown-all en jupyter_kernel.
README.md renombrado a README.txt para evitar error de parseo del indexer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:10:23 +02:00
egutierrez 7eef2544ab feat: add C++ ImGui functions for core UI and visualization
Funciones C++/ImGui para dashboards (grid, panel, docking, sidebar, tabs),
visualizaciones (candlestick, gauge, histogram, pie, sparkline, heatmap,
scatter, line, bar, surface3d, kpi, table), grafos (force layout, renderer,
viewport, spatial hash, types) y utilidades (time series buffer, tracy zones,
memory/fps overlay, plot theme).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:10:18 +02:00
102 changed files with 280541 additions and 17 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
+367
View File
@@ -0,0 +1,367 @@
# /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.
El prefijo `service:` indica que la app es un proceso de larga duracion (API, daemon, watcher). Añadir tag `service` automaticamente.
---
## 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|cpp}
domain: {domain}
description: "{descripcion}"
tags: [{tags}] # Añadir "service" si es proceso de larga duracion
uses_functions:
- {id_funcion_1}
- {id_funcion_2}
uses_types: []
framework: "{bubbletea|wails|httpx|imgui|...}"
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}
```
**Si es un service** (tag `service`), documentar ademas en el app.md:
- Puerto que usa (si expone HTTP/gRPC)
- Como lanzarlo y pararlo
- Health check (como comprobar que esta vivo)
### .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) — OBLIGATORIO
Toda app nueva DEBE publicarse en Gitea. Este paso NO es opcional.
**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
### App C++ (ImGui)
1. Codigo fuente va en `apps/{app_name}/` (no en `cpp/apps/`)
2. `cpp/CMakeLists.txt` referencia la app con `add_subdirectory(../apps/{app_name} ...)`
3. Funciones C++ del registry se incluyen como .cpp en el CMakeLists.txt de la app
4. Para Windows: cross-compile con `cmake -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake`
### Service (tag `service`)
1. Detectar si el usuario pide un servicio (API, daemon, watcher, server) o usa prefijo `service:`
2. Añadir tag `service` al array `tags` del app.md
3. Documentar en app.md: puerto, como lanzar/parar, health check
4. Estructura tipica para un HTTP service en Go:
```
apps/{service_name}/
├── app.md # tags: [service, api, ...]
├── main.go # Bind port, listen, graceful shutdown
├── handlers.go # HTTP handlers que componen funciones del registry
├── go.mod
├── .gitignore
```
5. El service se ejecuta como: `go run . --port 8080`
6. Para consultar services existentes: `sqlite3 registry.db "SELECT id, name, description FROM apps WHERE tags LIKE '%service%';"`
---
## Reglas
- **Codigo reutilizable** va en `functions/`, NO en la app → usar fn-constructor
- **Codigo especifico** de la app va en `apps/{app_name}/`
- **Todas las apps van en `apps/`**, incluidas C++, TypeScript, etc. Nunca en `cpp/apps/` ni otros subdirectorios
- **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 — **verificar que aparece en registry.db**
- Siempre auditar con fn-recopilador antes de publicar
- **Siempre publicar en Gitea** (PASO 5) — toda app tiene repo en `dataforge/{app_name}`
- **Siempre actualizar `repo_url`** en app.md despues de publicar y re-indexar
- **Tag `service`**: añadir a apps que son procesos de larga duracion (APIs, daemons, watchers, schedulers)
- **Tag `launcher`**: añadir a pipelines que deben aparecer en Pipeline Launcher TUI
$ARGUMENTS
+273
View File
@@ -0,0 +1,273 @@
# /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
- **Tags con significado especial** — ver `.claude/rules/function_tags.md`:
- `launcher`: pipelines que deben aparecer en Pipeline Launcher TUI. Añadir cuando se crea un pipeline ejecutable desde el launcher. NO añadir a pipelines interactivos/TUIs.
- `service`: para apps que son procesos de larga duracion (usado en /app, no en funciones)
$ARGUMENTS
+1 -1
View File
@@ -11,7 +11,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 05 | [stubs.md](stubs.md) | Stubs impuros para dependencias externas | | 05 | [stubs.md](stubs.md) | Stubs impuros para dependencias externas |
| 06 | [assertions.md](assertions.md) | Kinds de assertions son texto libre | | 06 | [assertions.md](assertions.md) | Kinds de assertions son texto libre |
| 07 | [proposals.md](proposals.md) | Quien crea proposals y cuando | | 07 | [proposals.md](proposals.md) | Quien crea proposals y cuando |
| 08 | [tag_launcher.md](tag_launcher.md) | Tag launcher para Pipeline Launcher TUI | | 08 | [function_tags.md](function_tags.md) | Tags con significado especial: launcher, service |
| 09 | [go_packages.md](go_packages.md) | Nombre de paquete Go = nombre del directorio | | 09 | [go_packages.md](go_packages.md) | Nombre de paquete Go = nombre del directorio |
| 10 | [apps_vs_functions.md](apps_vs_functions.md) | Codigo reutilizable en functions/, no reutilizable en apps/ | | 10 | [apps_vs_functions.md](apps_vs_functions.md) | Codigo reutilizable en functions/, no reutilizable en apps/ |
| 11 | [sources.md](sources.md) | Extraccion de funciones desde repos externos | | 11 | [sources.md](sources.md) | Extraccion de funciones desde repos externos |
+30
View File
@@ -0,0 +1,30 @@
Los pipelines con tag `launcher` aparecen en el Pipeline Launcher TUI (`apps/pipeline_launcher`).
Sin el tag, el pipeline no es lanzable desde la TUI. Añadir `launcher` al array `tags` del .md al crear un pipeline ejecutable desde el launcher.
Pipelines interactivos (TUIs) o que no son subprocesos NO deben llevar este tag.
## Tag `service`
Las apps con tag `service` son procesos de larga duracion: APIs, daemons, watchers, servers.
Diferencia con una app normal:
- Una **app** se ejecuta, hace su trabajo, y termina (CLI, TUI, script)
- Un **service** se lanza y queda corriendo indefinidamente (API server, scheduler, watcher)
Añadir `service` al array `tags` del `app.md` cuando la app esta diseñada para correr como proceso persistente.
Un service sigue siendo una app — vive en `apps/`, tiene `app.md`, se indexa igual. El tag es solo metadata para filtrar:
```sql
-- Listar services
SELECT id, name, description FROM apps WHERE tags LIKE '%service%';
-- Listar apps que NO son services
SELECT id, name, description FROM apps WHERE tags NOT LIKE '%service%';
```
Documentar en el `app.md` del service:
- El puerto que usa (si expone HTTP/gRPC)
- Como lanzarlo y pararlo
- Como comprobar que esta vivo (health check)
-5
View File
@@ -1,5 +0,0 @@
Los pipelines con tag `launcher` aparecen en el Pipeline Launcher TUI (`apps/pipeline_launcher`).
Sin el tag, el pipeline no es lanzable desde la TUI. Añadir `launcher` al array `tags` del .md al crear un pipeline ejecutable desde el launcher.
Pipelines interactivos (TUIs) o que no son subprocesos NO deben llevar este tag.
+1
View File
@@ -54,3 +54,4 @@ Thumbs.db
.local .local
broken_paths.txt broken_paths.txt
imgui.ini
+6 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(fn_registry_cpp LANGUAGES CXX) project(fn_registry_cpp LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -98,3 +98,8 @@ endfunction()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt) if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt)
add_subdirectory(apps/chart_demo) add_subdirectory(apps/chart_demo)
endif() endif()
# --- Registry Dashboard (lives in apps/ per project convention) ---
if(EXISTS ${CMAKE_SOURCE_DIR}/../apps/registry_dashboard/CMakeLists.txt)
add_subdirectory(${CMAKE_SOURCE_DIR}/../apps/registry_dashboard ${CMAKE_BINARY_DIR}/apps/registry_dashboard)
endif()
+20
View File
@@ -52,8 +52,20 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
if (config.viewports) {
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
}
ImGui::StyleColorsDark(); ImGui::StyleColorsDark();
// When viewports are enabled, tweak WindowRounding/WindowBg so
// platform windows look consistent with the main window
if (config.viewports) {
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330"); ImGui_ImplOpenGL3_Init("#version 330");
@@ -80,6 +92,14 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// Multi-viewport: update and render platform windows
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
GLFWwindow* backup_ctx = glfwGetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
glfwMakeContextCurrent(backup_ctx);
}
glfwSwapBuffers(window); glfwSwapBuffers(window);
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
+1
View File
@@ -9,6 +9,7 @@ struct AppConfig {
int width = 1280; int width = 1280;
int height = 720; int height = 720;
bool vsync = true; bool vsync = true;
bool viewports = false; // Enable multi-viewport: ImGui windows become real OS windows
float bg_r = 0.1f; float bg_r = 0.1f;
float bg_g = 0.1f; float bg_g = 0.1f;
float bg_b = 0.1f; float bg_b = 0.1f;
+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*`.
+19
View File
@@ -0,0 +1,19 @@
#include "core/fullscreen_window.h"
#include "imgui.h"
bool fullscreen_window_begin(const char* id) {
const ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(vp->WorkPos);
ImGui::SetNextWindowSize(vp->WorkSize);
return ImGui::Begin(id, nullptr,
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus);
}
void fullscreen_window_end() {
ImGui::End();
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
// fullscreen_window — ImGui window that covers the entire viewport.
// No title bar, no resize, no move, no collapse. Supports scrolling.
//
// Usage:
// if (fullscreen_window_begin()) {
// // render content here
// }
// fullscreen_window_end(); // ALWAYS call, even if begin returned false
//
// The default id "##fullscreen" is invisible (## prefix suppresses display).
// Use a different id if you need multiple fullscreen windows stacked.
bool fullscreen_window_begin(const char* id = "##fullscreen");
void fullscreen_window_end();
+66
View File
@@ -0,0 +1,66 @@
---
name: fullscreen_window
kind: component
lang: cpp
domain: core
version: "0.1.0"
purity: pure
signature: "bool fullscreen_window_begin(const char* id = \"##fullscreen\"); void fullscreen_window_end()"
description: "Ventana ImGui fullscreen sin decoraciones que ocupa todo el viewport, elimina la necesidad de usar el sistema de ventanas interno"
tags: [imgui, layout, fullscreen, window]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/fullscreen_window.cpp"
framework: imgui
params:
- name: id
desc: "Identificador ImGui de la ventana, default ##fullscreen (el prefijo ## oculta el texto del titulo)"
output: "true si la ventana es visible (siempre true en fullscreen); llamar siempre fullscreen_window_end() independientemente del valor de retorno"
---
# fullscreen_window
Wrapper que crea una ventana ImGui que ocupa exactamente el viewport de trabajo (`WorkPos` / `WorkSize`). Elimina todas las decoraciones: title bar, resize grip, move, collapse. Ideal como capa raiz de una aplicacion ImGui donde el contenido propio gestiona el layout.
## Uso
```cpp
if (fullscreen_window_begin()) {
// todo el layout de la app va aqui
dashboard_grid_begin(3, 8.0f);
// ...
dashboard_grid_end();
}
fullscreen_window_end(); // siempre llamar
```
Con ID explicito (si se necesitan multiples capas):
```cpp
if (fullscreen_window_begin("##background")) {
render_background();
}
fullscreen_window_end();
```
## Implementacion
- `GetMainViewport()` obtiene el viewport principal (compatible con viewports multi-monitor de ImGui)
- `SetNextWindowPos(vp->WorkPos)` posiciona en el area de trabajo (excluye menu bars del OS)
- `SetNextWindowSize(vp->WorkSize)` ocupa exactamente el area disponible
- Flags: `NoTitleBar | NoResize | NoMove | NoCollapse | NoBringToFrontOnFocus | NoNavFocus`
- `NoBringToFrontOnFocus` y `NoNavFocus` evitan que la ventana fullscreen robe el foco de ventanas superpuestas
## Notas
- Pura en el sentido de que no hace I/O ni tiene estado propio; solo configura el estado next-frame de ImGui.
- `WorkPos`/`WorkSize` respetan los menu bars del sistema operativo (en plataformas que los tienen). Para ocupar literalmente toda la pantalla usar `Pos`/`Size` del viewport.
- Compatible con `dashboard_grid` y `dashboard_panel`: el fullscreen_window actua como contenedor raiz y los paneles/grids se renderizan dentro.
- El patron begin/end es idiomatico en ImGui: `end` debe llamarse siempre para cerrar la ventana correctamente, aunque `begin` retorne false.
+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.
+30
View File
@@ -0,0 +1,30 @@
#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) {
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, static_cast<double>(-radius), "%.1f", 90.0);
} else {
float r = (radius > 0.0f) ? radius : 0.4f;
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, static_cast<double>(r), "%.1f", 90.0);
}
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);
} else {
double r = (radius > 0.0) ? radius : 0.4;
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0);
}
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.
+257679
View File
File diff suppressed because it is too large Load Diff
+13425
View File
File diff suppressed because it is too large Load Diff
+719
View File
@@ -0,0 +1,719 @@
/*
** 2006 June 7
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
*************************************************************************
** This header file defines the SQLite interface for use by
** shared libraries that want to be imported as extensions into
** an SQLite instance. Shared libraries that intend to be loaded
** as extensions by SQLite should #include this file instead of
** sqlite3.h.
*/
#ifndef SQLITE3EXT_H
#define SQLITE3EXT_H
#include "sqlite3.h"
/*
** The following structure holds pointers to all of the SQLite API
** routines.
**
** WARNING: In order to maintain backwards compatibility, add new
** interfaces to the end of this structure only. If you insert new
** interfaces in the middle of this structure, then older different
** versions of SQLite will not be able to load each other's shared
** libraries!
*/
struct sqlite3_api_routines {
void * (*aggregate_context)(sqlite3_context*,int nBytes);
int (*aggregate_count)(sqlite3_context*);
int (*bind_blob)(sqlite3_stmt*,int,const void*,int n,void(*)(void*));
int (*bind_double)(sqlite3_stmt*,int,double);
int (*bind_int)(sqlite3_stmt*,int,int);
int (*bind_int64)(sqlite3_stmt*,int,sqlite_int64);
int (*bind_null)(sqlite3_stmt*,int);
int (*bind_parameter_count)(sqlite3_stmt*);
int (*bind_parameter_index)(sqlite3_stmt*,const char*zName);
const char * (*bind_parameter_name)(sqlite3_stmt*,int);
int (*bind_text)(sqlite3_stmt*,int,const char*,int n,void(*)(void*));
int (*bind_text16)(sqlite3_stmt*,int,const void*,int,void(*)(void*));
int (*bind_value)(sqlite3_stmt*,int,const sqlite3_value*);
int (*busy_handler)(sqlite3*,int(*)(void*,int),void*);
int (*busy_timeout)(sqlite3*,int ms);
int (*changes)(sqlite3*);
int (*close)(sqlite3*);
int (*collation_needed)(sqlite3*,void*,void(*)(void*,sqlite3*,
int eTextRep,const char*));
int (*collation_needed16)(sqlite3*,void*,void(*)(void*,sqlite3*,
int eTextRep,const void*));
const void * (*column_blob)(sqlite3_stmt*,int iCol);
int (*column_bytes)(sqlite3_stmt*,int iCol);
int (*column_bytes16)(sqlite3_stmt*,int iCol);
int (*column_count)(sqlite3_stmt*pStmt);
const char * (*column_database_name)(sqlite3_stmt*,int);
const void * (*column_database_name16)(sqlite3_stmt*,int);
const char * (*column_decltype)(sqlite3_stmt*,int i);
const void * (*column_decltype16)(sqlite3_stmt*,int);
double (*column_double)(sqlite3_stmt*,int iCol);
int (*column_int)(sqlite3_stmt*,int iCol);
sqlite_int64 (*column_int64)(sqlite3_stmt*,int iCol);
const char * (*column_name)(sqlite3_stmt*,int);
const void * (*column_name16)(sqlite3_stmt*,int);
const char * (*column_origin_name)(sqlite3_stmt*,int);
const void * (*column_origin_name16)(sqlite3_stmt*,int);
const char * (*column_table_name)(sqlite3_stmt*,int);
const void * (*column_table_name16)(sqlite3_stmt*,int);
const unsigned char * (*column_text)(sqlite3_stmt*,int iCol);
const void * (*column_text16)(sqlite3_stmt*,int iCol);
int (*column_type)(sqlite3_stmt*,int iCol);
sqlite3_value* (*column_value)(sqlite3_stmt*,int iCol);
void * (*commit_hook)(sqlite3*,int(*)(void*),void*);
int (*complete)(const char*sql);
int (*complete16)(const void*sql);
int (*create_collation)(sqlite3*,const char*,int,void*,
int(*)(void*,int,const void*,int,const void*));
int (*create_collation16)(sqlite3*,const void*,int,void*,
int(*)(void*,int,const void*,int,const void*));
int (*create_function)(sqlite3*,const char*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*));
int (*create_function16)(sqlite3*,const void*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*));
int (*create_module)(sqlite3*,const char*,const sqlite3_module*,void*);
int (*data_count)(sqlite3_stmt*pStmt);
sqlite3 * (*db_handle)(sqlite3_stmt*);
int (*declare_vtab)(sqlite3*,const char*);
int (*enable_shared_cache)(int);
int (*errcode)(sqlite3*db);
const char * (*errmsg)(sqlite3*);
const void * (*errmsg16)(sqlite3*);
int (*exec)(sqlite3*,const char*,sqlite3_callback,void*,char**);
int (*expired)(sqlite3_stmt*);
int (*finalize)(sqlite3_stmt*pStmt);
void (*free)(void*);
void (*free_table)(char**result);
int (*get_autocommit)(sqlite3*);
void * (*get_auxdata)(sqlite3_context*,int);
int (*get_table)(sqlite3*,const char*,char***,int*,int*,char**);
int (*global_recover)(void);
void (*interruptx)(sqlite3*);
sqlite_int64 (*last_insert_rowid)(sqlite3*);
const char * (*libversion)(void);
int (*libversion_number)(void);
void *(*malloc)(int);
char * (*mprintf)(const char*,...);
int (*open)(const char*,sqlite3**);
int (*open16)(const void*,sqlite3**);
int (*prepare)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
int (*prepare16)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
void * (*profile)(sqlite3*,void(*)(void*,const char*,sqlite_uint64),void*);
void (*progress_handler)(sqlite3*,int,int(*)(void*),void*);
void *(*realloc)(void*,int);
int (*reset)(sqlite3_stmt*pStmt);
void (*result_blob)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_double)(sqlite3_context*,double);
void (*result_error)(sqlite3_context*,const char*,int);
void (*result_error16)(sqlite3_context*,const void*,int);
void (*result_int)(sqlite3_context*,int);
void (*result_int64)(sqlite3_context*,sqlite_int64);
void (*result_null)(sqlite3_context*);
void (*result_text)(sqlite3_context*,const char*,int,void(*)(void*));
void (*result_text16)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_text16be)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_text16le)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_value)(sqlite3_context*,sqlite3_value*);
void * (*rollback_hook)(sqlite3*,void(*)(void*),void*);
int (*set_authorizer)(sqlite3*,int(*)(void*,int,const char*,const char*,
const char*,const char*),void*);
void (*set_auxdata)(sqlite3_context*,int,void*,void (*)(void*));
char * (*xsnprintf)(int,char*,const char*,...);
int (*step)(sqlite3_stmt*);
int (*table_column_metadata)(sqlite3*,const char*,const char*,const char*,
char const**,char const**,int*,int*,int*);
void (*thread_cleanup)(void);
int (*total_changes)(sqlite3*);
void * (*trace)(sqlite3*,void(*xTrace)(void*,const char*),void*);
int (*transfer_bindings)(sqlite3_stmt*,sqlite3_stmt*);
void * (*update_hook)(sqlite3*,void(*)(void*,int ,char const*,char const*,
sqlite_int64),void*);
void * (*user_data)(sqlite3_context*);
const void * (*value_blob)(sqlite3_value*);
int (*value_bytes)(sqlite3_value*);
int (*value_bytes16)(sqlite3_value*);
double (*value_double)(sqlite3_value*);
int (*value_int)(sqlite3_value*);
sqlite_int64 (*value_int64)(sqlite3_value*);
int (*value_numeric_type)(sqlite3_value*);
const unsigned char * (*value_text)(sqlite3_value*);
const void * (*value_text16)(sqlite3_value*);
const void * (*value_text16be)(sqlite3_value*);
const void * (*value_text16le)(sqlite3_value*);
int (*value_type)(sqlite3_value*);
char *(*vmprintf)(const char*,va_list);
/* Added ??? */
int (*overload_function)(sqlite3*, const char *zFuncName, int nArg);
/* Added by 3.3.13 */
int (*prepare_v2)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
int (*prepare16_v2)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
int (*clear_bindings)(sqlite3_stmt*);
/* Added by 3.4.1 */
int (*create_module_v2)(sqlite3*,const char*,const sqlite3_module*,void*,
void (*xDestroy)(void *));
/* Added by 3.5.0 */
int (*bind_zeroblob)(sqlite3_stmt*,int,int);
int (*blob_bytes)(sqlite3_blob*);
int (*blob_close)(sqlite3_blob*);
int (*blob_open)(sqlite3*,const char*,const char*,const char*,sqlite3_int64,
int,sqlite3_blob**);
int (*blob_read)(sqlite3_blob*,void*,int,int);
int (*blob_write)(sqlite3_blob*,const void*,int,int);
int (*create_collation_v2)(sqlite3*,const char*,int,void*,
int(*)(void*,int,const void*,int,const void*),
void(*)(void*));
int (*file_control)(sqlite3*,const char*,int,void*);
sqlite3_int64 (*memory_highwater)(int);
sqlite3_int64 (*memory_used)(void);
sqlite3_mutex *(*mutex_alloc)(int);
void (*mutex_enter)(sqlite3_mutex*);
void (*mutex_free)(sqlite3_mutex*);
void (*mutex_leave)(sqlite3_mutex*);
int (*mutex_try)(sqlite3_mutex*);
int (*open_v2)(const char*,sqlite3**,int,const char*);
int (*release_memory)(int);
void (*result_error_nomem)(sqlite3_context*);
void (*result_error_toobig)(sqlite3_context*);
int (*sleep)(int);
void (*soft_heap_limit)(int);
sqlite3_vfs *(*vfs_find)(const char*);
int (*vfs_register)(sqlite3_vfs*,int);
int (*vfs_unregister)(sqlite3_vfs*);
int (*xthreadsafe)(void);
void (*result_zeroblob)(sqlite3_context*,int);
void (*result_error_code)(sqlite3_context*,int);
int (*test_control)(int, ...);
void (*randomness)(int,void*);
sqlite3 *(*context_db_handle)(sqlite3_context*);
int (*extended_result_codes)(sqlite3*,int);
int (*limit)(sqlite3*,int,int);
sqlite3_stmt *(*next_stmt)(sqlite3*,sqlite3_stmt*);
const char *(*sql)(sqlite3_stmt*);
int (*status)(int,int*,int*,int);
int (*backup_finish)(sqlite3_backup*);
sqlite3_backup *(*backup_init)(sqlite3*,const char*,sqlite3*,const char*);
int (*backup_pagecount)(sqlite3_backup*);
int (*backup_remaining)(sqlite3_backup*);
int (*backup_step)(sqlite3_backup*,int);
const char *(*compileoption_get)(int);
int (*compileoption_used)(const char*);
int (*create_function_v2)(sqlite3*,const char*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*),
void(*xDestroy)(void*));
int (*db_config)(sqlite3*,int,...);
sqlite3_mutex *(*db_mutex)(sqlite3*);
int (*db_status)(sqlite3*,int,int*,int*,int);
int (*extended_errcode)(sqlite3*);
void (*log)(int,const char*,...);
sqlite3_int64 (*soft_heap_limit64)(sqlite3_int64);
const char *(*sourceid)(void);
int (*stmt_status)(sqlite3_stmt*,int,int);
int (*strnicmp)(const char*,const char*,int);
int (*unlock_notify)(sqlite3*,void(*)(void**,int),void*);
int (*wal_autocheckpoint)(sqlite3*,int);
int (*wal_checkpoint)(sqlite3*,const char*);
void *(*wal_hook)(sqlite3*,int(*)(void*,sqlite3*,const char*,int),void*);
int (*blob_reopen)(sqlite3_blob*,sqlite3_int64);
int (*vtab_config)(sqlite3*,int op,...);
int (*vtab_on_conflict)(sqlite3*);
/* Version 3.7.16 and later */
int (*close_v2)(sqlite3*);
const char *(*db_filename)(sqlite3*,const char*);
int (*db_readonly)(sqlite3*,const char*);
int (*db_release_memory)(sqlite3*);
const char *(*errstr)(int);
int (*stmt_busy)(sqlite3_stmt*);
int (*stmt_readonly)(sqlite3_stmt*);
int (*stricmp)(const char*,const char*);
int (*uri_boolean)(const char*,const char*,int);
sqlite3_int64 (*uri_int64)(const char*,const char*,sqlite3_int64);
const char *(*uri_parameter)(const char*,const char*);
char *(*xvsnprintf)(int,char*,const char*,va_list);
int (*wal_checkpoint_v2)(sqlite3*,const char*,int,int*,int*);
/* Version 3.8.7 and later */
int (*auto_extension)(void(*)(void));
int (*bind_blob64)(sqlite3_stmt*,int,const void*,sqlite3_uint64,
void(*)(void*));
int (*bind_text64)(sqlite3_stmt*,int,const char*,sqlite3_uint64,
void(*)(void*),unsigned char);
int (*cancel_auto_extension)(void(*)(void));
int (*load_extension)(sqlite3*,const char*,const char*,char**);
void *(*malloc64)(sqlite3_uint64);
sqlite3_uint64 (*msize)(void*);
void *(*realloc64)(void*,sqlite3_uint64);
void (*reset_auto_extension)(void);
void (*result_blob64)(sqlite3_context*,const void*,sqlite3_uint64,
void(*)(void*));
void (*result_text64)(sqlite3_context*,const char*,sqlite3_uint64,
void(*)(void*), unsigned char);
int (*strglob)(const char*,const char*);
/* Version 3.8.11 and later */
sqlite3_value *(*value_dup)(const sqlite3_value*);
void (*value_free)(sqlite3_value*);
int (*result_zeroblob64)(sqlite3_context*,sqlite3_uint64);
int (*bind_zeroblob64)(sqlite3_stmt*, int, sqlite3_uint64);
/* Version 3.9.0 and later */
unsigned int (*value_subtype)(sqlite3_value*);
void (*result_subtype)(sqlite3_context*,unsigned int);
/* Version 3.10.0 and later */
int (*status64)(int,sqlite3_int64*,sqlite3_int64*,int);
int (*strlike)(const char*,const char*,unsigned int);
int (*db_cacheflush)(sqlite3*);
/* Version 3.12.0 and later */
int (*system_errno)(sqlite3*);
/* Version 3.14.0 and later */
int (*trace_v2)(sqlite3*,unsigned,int(*)(unsigned,void*,void*,void*),void*);
char *(*expanded_sql)(sqlite3_stmt*);
/* Version 3.18.0 and later */
void (*set_last_insert_rowid)(sqlite3*,sqlite3_int64);
/* Version 3.20.0 and later */
int (*prepare_v3)(sqlite3*,const char*,int,unsigned int,
sqlite3_stmt**,const char**);
int (*prepare16_v3)(sqlite3*,const void*,int,unsigned int,
sqlite3_stmt**,const void**);
int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*));
void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
void *(*value_pointer)(sqlite3_value*,const char*);
int (*vtab_nochange)(sqlite3_context*);
int (*value_nochange)(sqlite3_value*);
const char *(*vtab_collation)(sqlite3_index_info*,int);
/* Version 3.24.0 and later */
int (*keyword_count)(void);
int (*keyword_name)(int,const char**,int*);
int (*keyword_check)(const char*,int);
sqlite3_str *(*str_new)(sqlite3*);
char *(*str_finish)(sqlite3_str*);
void (*str_appendf)(sqlite3_str*, const char *zFormat, ...);
void (*str_vappendf)(sqlite3_str*, const char *zFormat, va_list);
void (*str_append)(sqlite3_str*, const char *zIn, int N);
void (*str_appendall)(sqlite3_str*, const char *zIn);
void (*str_appendchar)(sqlite3_str*, int N, char C);
void (*str_reset)(sqlite3_str*);
int (*str_errcode)(sqlite3_str*);
int (*str_length)(sqlite3_str*);
char *(*str_value)(sqlite3_str*);
/* Version 3.25.0 and later */
int (*create_window_function)(sqlite3*,const char*,int,int,void*,
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*),
void (*xValue)(sqlite3_context*),
void (*xInv)(sqlite3_context*,int,sqlite3_value**),
void(*xDestroy)(void*));
/* Version 3.26.0 and later */
const char *(*normalized_sql)(sqlite3_stmt*);
/* Version 3.28.0 and later */
int (*stmt_isexplain)(sqlite3_stmt*);
int (*value_frombind)(sqlite3_value*);
/* Version 3.30.0 and later */
int (*drop_modules)(sqlite3*,const char**);
/* Version 3.31.0 and later */
sqlite3_int64 (*hard_heap_limit64)(sqlite3_int64);
const char *(*uri_key)(const char*,int);
const char *(*filename_database)(const char*);
const char *(*filename_journal)(const char*);
const char *(*filename_wal)(const char*);
/* Version 3.32.0 and later */
const char *(*create_filename)(const char*,const char*,const char*,
int,const char**);
void (*free_filename)(const char*);
sqlite3_file *(*database_file_object)(const char*);
/* Version 3.34.0 and later */
int (*txn_state)(sqlite3*,const char*);
/* Version 3.36.1 and later */
sqlite3_int64 (*changes64)(sqlite3*);
sqlite3_int64 (*total_changes64)(sqlite3*);
/* Version 3.37.0 and later */
int (*autovacuum_pages)(sqlite3*,
unsigned int(*)(void*,const char*,unsigned int,unsigned int,unsigned int),
void*, void(*)(void*));
/* Version 3.38.0 and later */
int (*error_offset)(sqlite3*);
int (*vtab_rhs_value)(sqlite3_index_info*,int,sqlite3_value**);
int (*vtab_distinct)(sqlite3_index_info*);
int (*vtab_in)(sqlite3_index_info*,int,int);
int (*vtab_in_first)(sqlite3_value*,sqlite3_value**);
int (*vtab_in_next)(sqlite3_value*,sqlite3_value**);
/* Version 3.39.0 and later */
int (*deserialize)(sqlite3*,const char*,unsigned char*,
sqlite3_int64,sqlite3_int64,unsigned);
unsigned char *(*serialize)(sqlite3*,const char *,sqlite3_int64*,
unsigned int);
const char *(*db_name)(sqlite3*,int);
/* Version 3.40.0 and later */
int (*value_encoding)(sqlite3_value*);
/* Version 3.41.0 and later */
int (*is_interrupted)(sqlite3*);
/* Version 3.43.0 and later */
int (*stmt_explain)(sqlite3_stmt*,int);
/* Version 3.44.0 and later */
void *(*get_clientdata)(sqlite3*,const char*);
int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
};
/*
** This is the function signature used for all extension entry points. It
** is also defined in the file "loadext.c".
*/
typedef int (*sqlite3_loadext_entry)(
sqlite3 *db, /* Handle to the database. */
char **pzErrMsg, /* Used to set error string on failure. */
const sqlite3_api_routines *pThunk /* Extension API function pointers. */
);
/*
** The following macros redefine the API routines so that they are
** redirected through the global sqlite3_api structure.
**
** This header file is also used by the loadext.c source file
** (part of the main SQLite library - not an extension) so that
** it can get access to the sqlite3_api_routines structure
** definition. But the main library does not want to redefine
** the API. So the redefinition macros are only valid if the
** SQLITE_CORE macros is undefined.
*/
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
#define sqlite3_aggregate_context sqlite3_api->aggregate_context
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_aggregate_count sqlite3_api->aggregate_count
#endif
#define sqlite3_bind_blob sqlite3_api->bind_blob
#define sqlite3_bind_double sqlite3_api->bind_double
#define sqlite3_bind_int sqlite3_api->bind_int
#define sqlite3_bind_int64 sqlite3_api->bind_int64
#define sqlite3_bind_null sqlite3_api->bind_null
#define sqlite3_bind_parameter_count sqlite3_api->bind_parameter_count
#define sqlite3_bind_parameter_index sqlite3_api->bind_parameter_index
#define sqlite3_bind_parameter_name sqlite3_api->bind_parameter_name
#define sqlite3_bind_text sqlite3_api->bind_text
#define sqlite3_bind_text16 sqlite3_api->bind_text16
#define sqlite3_bind_value sqlite3_api->bind_value
#define sqlite3_busy_handler sqlite3_api->busy_handler
#define sqlite3_busy_timeout sqlite3_api->busy_timeout
#define sqlite3_changes sqlite3_api->changes
#define sqlite3_close sqlite3_api->close
#define sqlite3_collation_needed sqlite3_api->collation_needed
#define sqlite3_collation_needed16 sqlite3_api->collation_needed16
#define sqlite3_column_blob sqlite3_api->column_blob
#define sqlite3_column_bytes sqlite3_api->column_bytes
#define sqlite3_column_bytes16 sqlite3_api->column_bytes16
#define sqlite3_column_count sqlite3_api->column_count
#define sqlite3_column_database_name sqlite3_api->column_database_name
#define sqlite3_column_database_name16 sqlite3_api->column_database_name16
#define sqlite3_column_decltype sqlite3_api->column_decltype
#define sqlite3_column_decltype16 sqlite3_api->column_decltype16
#define sqlite3_column_double sqlite3_api->column_double
#define sqlite3_column_int sqlite3_api->column_int
#define sqlite3_column_int64 sqlite3_api->column_int64
#define sqlite3_column_name sqlite3_api->column_name
#define sqlite3_column_name16 sqlite3_api->column_name16
#define sqlite3_column_origin_name sqlite3_api->column_origin_name
#define sqlite3_column_origin_name16 sqlite3_api->column_origin_name16
#define sqlite3_column_table_name sqlite3_api->column_table_name
#define sqlite3_column_table_name16 sqlite3_api->column_table_name16
#define sqlite3_column_text sqlite3_api->column_text
#define sqlite3_column_text16 sqlite3_api->column_text16
#define sqlite3_column_type sqlite3_api->column_type
#define sqlite3_column_value sqlite3_api->column_value
#define sqlite3_commit_hook sqlite3_api->commit_hook
#define sqlite3_complete sqlite3_api->complete
#define sqlite3_complete16 sqlite3_api->complete16
#define sqlite3_create_collation sqlite3_api->create_collation
#define sqlite3_create_collation16 sqlite3_api->create_collation16
#define sqlite3_create_function sqlite3_api->create_function
#define sqlite3_create_function16 sqlite3_api->create_function16
#define sqlite3_create_module sqlite3_api->create_module
#define sqlite3_create_module_v2 sqlite3_api->create_module_v2
#define sqlite3_data_count sqlite3_api->data_count
#define sqlite3_db_handle sqlite3_api->db_handle
#define sqlite3_declare_vtab sqlite3_api->declare_vtab
#define sqlite3_enable_shared_cache sqlite3_api->enable_shared_cache
#define sqlite3_errcode sqlite3_api->errcode
#define sqlite3_errmsg sqlite3_api->errmsg
#define sqlite3_errmsg16 sqlite3_api->errmsg16
#define sqlite3_exec sqlite3_api->exec
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_expired sqlite3_api->expired
#endif
#define sqlite3_finalize sqlite3_api->finalize
#define sqlite3_free sqlite3_api->free
#define sqlite3_free_table sqlite3_api->free_table
#define sqlite3_get_autocommit sqlite3_api->get_autocommit
#define sqlite3_get_auxdata sqlite3_api->get_auxdata
#define sqlite3_get_table sqlite3_api->get_table
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_global_recover sqlite3_api->global_recover
#endif
#define sqlite3_interrupt sqlite3_api->interruptx
#define sqlite3_last_insert_rowid sqlite3_api->last_insert_rowid
#define sqlite3_libversion sqlite3_api->libversion
#define sqlite3_libversion_number sqlite3_api->libversion_number
#define sqlite3_malloc sqlite3_api->malloc
#define sqlite3_mprintf sqlite3_api->mprintf
#define sqlite3_open sqlite3_api->open
#define sqlite3_open16 sqlite3_api->open16
#define sqlite3_prepare sqlite3_api->prepare
#define sqlite3_prepare16 sqlite3_api->prepare16
#define sqlite3_prepare_v2 sqlite3_api->prepare_v2
#define sqlite3_prepare16_v2 sqlite3_api->prepare16_v2
#define sqlite3_profile sqlite3_api->profile
#define sqlite3_progress_handler sqlite3_api->progress_handler
#define sqlite3_realloc sqlite3_api->realloc
#define sqlite3_reset sqlite3_api->reset
#define sqlite3_result_blob sqlite3_api->result_blob
#define sqlite3_result_double sqlite3_api->result_double
#define sqlite3_result_error sqlite3_api->result_error
#define sqlite3_result_error16 sqlite3_api->result_error16
#define sqlite3_result_int sqlite3_api->result_int
#define sqlite3_result_int64 sqlite3_api->result_int64
#define sqlite3_result_null sqlite3_api->result_null
#define sqlite3_result_text sqlite3_api->result_text
#define sqlite3_result_text16 sqlite3_api->result_text16
#define sqlite3_result_text16be sqlite3_api->result_text16be
#define sqlite3_result_text16le sqlite3_api->result_text16le
#define sqlite3_result_value sqlite3_api->result_value
#define sqlite3_rollback_hook sqlite3_api->rollback_hook
#define sqlite3_set_authorizer sqlite3_api->set_authorizer
#define sqlite3_set_auxdata sqlite3_api->set_auxdata
#define sqlite3_snprintf sqlite3_api->xsnprintf
#define sqlite3_step sqlite3_api->step
#define sqlite3_table_column_metadata sqlite3_api->table_column_metadata
#define sqlite3_thread_cleanup sqlite3_api->thread_cleanup
#define sqlite3_total_changes sqlite3_api->total_changes
#define sqlite3_trace sqlite3_api->trace
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_transfer_bindings sqlite3_api->transfer_bindings
#endif
#define sqlite3_update_hook sqlite3_api->update_hook
#define sqlite3_user_data sqlite3_api->user_data
#define sqlite3_value_blob sqlite3_api->value_blob
#define sqlite3_value_bytes sqlite3_api->value_bytes
#define sqlite3_value_bytes16 sqlite3_api->value_bytes16
#define sqlite3_value_double sqlite3_api->value_double
#define sqlite3_value_int sqlite3_api->value_int
#define sqlite3_value_int64 sqlite3_api->value_int64
#define sqlite3_value_numeric_type sqlite3_api->value_numeric_type
#define sqlite3_value_text sqlite3_api->value_text
#define sqlite3_value_text16 sqlite3_api->value_text16
#define sqlite3_value_text16be sqlite3_api->value_text16be
#define sqlite3_value_text16le sqlite3_api->value_text16le
#define sqlite3_value_type sqlite3_api->value_type
#define sqlite3_vmprintf sqlite3_api->vmprintf
#define sqlite3_vsnprintf sqlite3_api->xvsnprintf
#define sqlite3_overload_function sqlite3_api->overload_function
#define sqlite3_prepare_v2 sqlite3_api->prepare_v2
#define sqlite3_prepare16_v2 sqlite3_api->prepare16_v2
#define sqlite3_clear_bindings sqlite3_api->clear_bindings
#define sqlite3_bind_zeroblob sqlite3_api->bind_zeroblob
#define sqlite3_blob_bytes sqlite3_api->blob_bytes
#define sqlite3_blob_close sqlite3_api->blob_close
#define sqlite3_blob_open sqlite3_api->blob_open
#define sqlite3_blob_read sqlite3_api->blob_read
#define sqlite3_blob_write sqlite3_api->blob_write
#define sqlite3_create_collation_v2 sqlite3_api->create_collation_v2
#define sqlite3_file_control sqlite3_api->file_control
#define sqlite3_memory_highwater sqlite3_api->memory_highwater
#define sqlite3_memory_used sqlite3_api->memory_used
#define sqlite3_mutex_alloc sqlite3_api->mutex_alloc
#define sqlite3_mutex_enter sqlite3_api->mutex_enter
#define sqlite3_mutex_free sqlite3_api->mutex_free
#define sqlite3_mutex_leave sqlite3_api->mutex_leave
#define sqlite3_mutex_try sqlite3_api->mutex_try
#define sqlite3_open_v2 sqlite3_api->open_v2
#define sqlite3_release_memory sqlite3_api->release_memory
#define sqlite3_result_error_nomem sqlite3_api->result_error_nomem
#define sqlite3_result_error_toobig sqlite3_api->result_error_toobig
#define sqlite3_sleep sqlite3_api->sleep
#define sqlite3_soft_heap_limit sqlite3_api->soft_heap_limit
#define sqlite3_vfs_find sqlite3_api->vfs_find
#define sqlite3_vfs_register sqlite3_api->vfs_register
#define sqlite3_vfs_unregister sqlite3_api->vfs_unregister
#define sqlite3_threadsafe sqlite3_api->xthreadsafe
#define sqlite3_result_zeroblob sqlite3_api->result_zeroblob
#define sqlite3_result_error_code sqlite3_api->result_error_code
#define sqlite3_test_control sqlite3_api->test_control
#define sqlite3_randomness sqlite3_api->randomness
#define sqlite3_context_db_handle sqlite3_api->context_db_handle
#define sqlite3_extended_result_codes sqlite3_api->extended_result_codes
#define sqlite3_limit sqlite3_api->limit
#define sqlite3_next_stmt sqlite3_api->next_stmt
#define sqlite3_sql sqlite3_api->sql
#define sqlite3_status sqlite3_api->status
#define sqlite3_backup_finish sqlite3_api->backup_finish
#define sqlite3_backup_init sqlite3_api->backup_init
#define sqlite3_backup_pagecount sqlite3_api->backup_pagecount
#define sqlite3_backup_remaining sqlite3_api->backup_remaining
#define sqlite3_backup_step sqlite3_api->backup_step
#define sqlite3_compileoption_get sqlite3_api->compileoption_get
#define sqlite3_compileoption_used sqlite3_api->compileoption_used
#define sqlite3_create_function_v2 sqlite3_api->create_function_v2
#define sqlite3_db_config sqlite3_api->db_config
#define sqlite3_db_mutex sqlite3_api->db_mutex
#define sqlite3_db_status sqlite3_api->db_status
#define sqlite3_extended_errcode sqlite3_api->extended_errcode
#define sqlite3_log sqlite3_api->log
#define sqlite3_soft_heap_limit64 sqlite3_api->soft_heap_limit64
#define sqlite3_sourceid sqlite3_api->sourceid
#define sqlite3_stmt_status sqlite3_api->stmt_status
#define sqlite3_strnicmp sqlite3_api->strnicmp
#define sqlite3_unlock_notify sqlite3_api->unlock_notify
#define sqlite3_wal_autocheckpoint sqlite3_api->wal_autocheckpoint
#define sqlite3_wal_checkpoint sqlite3_api->wal_checkpoint
#define sqlite3_wal_hook sqlite3_api->wal_hook
#define sqlite3_blob_reopen sqlite3_api->blob_reopen
#define sqlite3_vtab_config sqlite3_api->vtab_config
#define sqlite3_vtab_on_conflict sqlite3_api->vtab_on_conflict
/* Version 3.7.16 and later */
#define sqlite3_close_v2 sqlite3_api->close_v2
#define sqlite3_db_filename sqlite3_api->db_filename
#define sqlite3_db_readonly sqlite3_api->db_readonly
#define sqlite3_db_release_memory sqlite3_api->db_release_memory
#define sqlite3_errstr sqlite3_api->errstr
#define sqlite3_stmt_busy sqlite3_api->stmt_busy
#define sqlite3_stmt_readonly sqlite3_api->stmt_readonly
#define sqlite3_stricmp sqlite3_api->stricmp
#define sqlite3_uri_boolean sqlite3_api->uri_boolean
#define sqlite3_uri_int64 sqlite3_api->uri_int64
#define sqlite3_uri_parameter sqlite3_api->uri_parameter
#define sqlite3_uri_vsnprintf sqlite3_api->xvsnprintf
#define sqlite3_wal_checkpoint_v2 sqlite3_api->wal_checkpoint_v2
/* Version 3.8.7 and later */
#define sqlite3_auto_extension sqlite3_api->auto_extension
#define sqlite3_bind_blob64 sqlite3_api->bind_blob64
#define sqlite3_bind_text64 sqlite3_api->bind_text64
#define sqlite3_cancel_auto_extension sqlite3_api->cancel_auto_extension
#define sqlite3_load_extension sqlite3_api->load_extension
#define sqlite3_malloc64 sqlite3_api->malloc64
#define sqlite3_msize sqlite3_api->msize
#define sqlite3_realloc64 sqlite3_api->realloc64
#define sqlite3_reset_auto_extension sqlite3_api->reset_auto_extension
#define sqlite3_result_blob64 sqlite3_api->result_blob64
#define sqlite3_result_text64 sqlite3_api->result_text64
#define sqlite3_strglob sqlite3_api->strglob
/* Version 3.8.11 and later */
#define sqlite3_value_dup sqlite3_api->value_dup
#define sqlite3_value_free sqlite3_api->value_free
#define sqlite3_result_zeroblob64 sqlite3_api->result_zeroblob64
#define sqlite3_bind_zeroblob64 sqlite3_api->bind_zeroblob64
/* Version 3.9.0 and later */
#define sqlite3_value_subtype sqlite3_api->value_subtype
#define sqlite3_result_subtype sqlite3_api->result_subtype
/* Version 3.10.0 and later */
#define sqlite3_status64 sqlite3_api->status64
#define sqlite3_strlike sqlite3_api->strlike
#define sqlite3_db_cacheflush sqlite3_api->db_cacheflush
/* Version 3.12.0 and later */
#define sqlite3_system_errno sqlite3_api->system_errno
/* Version 3.14.0 and later */
#define sqlite3_trace_v2 sqlite3_api->trace_v2
#define sqlite3_expanded_sql sqlite3_api->expanded_sql
/* Version 3.18.0 and later */
#define sqlite3_set_last_insert_rowid sqlite3_api->set_last_insert_rowid
/* Version 3.20.0 and later */
#define sqlite3_prepare_v3 sqlite3_api->prepare_v3
#define sqlite3_prepare16_v3 sqlite3_api->prepare16_v3
#define sqlite3_bind_pointer sqlite3_api->bind_pointer
#define sqlite3_result_pointer sqlite3_api->result_pointer
#define sqlite3_value_pointer sqlite3_api->value_pointer
/* Version 3.22.0 and later */
#define sqlite3_vtab_nochange sqlite3_api->vtab_nochange
#define sqlite3_value_nochange sqlite3_api->value_nochange
#define sqlite3_vtab_collation sqlite3_api->vtab_collation
/* Version 3.24.0 and later */
#define sqlite3_keyword_count sqlite3_api->keyword_count
#define sqlite3_keyword_name sqlite3_api->keyword_name
#define sqlite3_keyword_check sqlite3_api->keyword_check
#define sqlite3_str_new sqlite3_api->str_new
#define sqlite3_str_finish sqlite3_api->str_finish
#define sqlite3_str_appendf sqlite3_api->str_appendf
#define sqlite3_str_vappendf sqlite3_api->str_vappendf
#define sqlite3_str_append sqlite3_api->str_append
#define sqlite3_str_appendall sqlite3_api->str_appendall
#define sqlite3_str_appendchar sqlite3_api->str_appendchar
#define sqlite3_str_reset sqlite3_api->str_reset
#define sqlite3_str_errcode sqlite3_api->str_errcode
#define sqlite3_str_length sqlite3_api->str_length
#define sqlite3_str_value sqlite3_api->str_value
/* Version 3.25.0 and later */
#define sqlite3_create_window_function sqlite3_api->create_window_function
/* Version 3.26.0 and later */
#define sqlite3_normalized_sql sqlite3_api->normalized_sql
/* Version 3.28.0 and later */
#define sqlite3_stmt_isexplain sqlite3_api->stmt_isexplain
#define sqlite3_value_frombind sqlite3_api->value_frombind
/* Version 3.30.0 and later */
#define sqlite3_drop_modules sqlite3_api->drop_modules
/* Version 3.31.0 and later */
#define sqlite3_hard_heap_limit64 sqlite3_api->hard_heap_limit64
#define sqlite3_uri_key sqlite3_api->uri_key
#define sqlite3_filename_database sqlite3_api->filename_database
#define sqlite3_filename_journal sqlite3_api->filename_journal
#define sqlite3_filename_wal sqlite3_api->filename_wal
/* Version 3.32.0 and later */
#define sqlite3_create_filename sqlite3_api->create_filename
#define sqlite3_free_filename sqlite3_api->free_filename
#define sqlite3_database_file_object sqlite3_api->database_file_object
/* Version 3.34.0 and later */
#define sqlite3_txn_state sqlite3_api->txn_state
/* Version 3.36.1 and later */
#define sqlite3_changes64 sqlite3_api->changes64
#define sqlite3_total_changes64 sqlite3_api->total_changes64
/* Version 3.37.0 and later */
#define sqlite3_autovacuum_pages sqlite3_api->autovacuum_pages
/* Version 3.38.0 and later */
#define sqlite3_error_offset sqlite3_api->error_offset
#define sqlite3_vtab_rhs_value sqlite3_api->vtab_rhs_value
#define sqlite3_vtab_distinct sqlite3_api->vtab_distinct
#define sqlite3_vtab_in sqlite3_api->vtab_in
#define sqlite3_vtab_in_first sqlite3_api->vtab_in_first
#define sqlite3_vtab_in_next sqlite3_api->vtab_in_next
/* Version 3.39.0 and later */
#ifndef SQLITE_OMIT_DESERIALIZE
#define sqlite3_deserialize sqlite3_api->deserialize
#define sqlite3_serialize sqlite3_api->serialize
#endif
#define sqlite3_db_name sqlite3_api->db_name
/* Version 3.40.0 and later */
#define sqlite3_value_encoding sqlite3_api->value_encoding
/* Version 3.41.0 and later */
#define sqlite3_is_interrupted sqlite3_api->is_interrupted
/* Version 3.43.0 and later */
#define sqlite3_stmt_explain sqlite3_api->stmt_explain
/* Version 3.44.0 and later */
#define sqlite3_get_clientdata sqlite3_api->get_clientdata
#define sqlite3_set_clientdata sqlite3_api->set_clientdata
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
/* This case when the file really is being compiled as a loadable
** extension */
# define SQLITE_EXTENSION_INIT1 const sqlite3_api_routines *sqlite3_api=0;
# define SQLITE_EXTENSION_INIT2(v) sqlite3_api=v;
# define SQLITE_EXTENSION_INIT3 \
extern const sqlite3_api_routines *sqlite3_api;
#else
/* This case when the file is being statically linked into the
** application */
# define SQLITE_EXTENSION_INIT1 /*no-op*/
# define SQLITE_EXTENSION_INIT2(v) (void)v; /* unused parameter */
# define SQLITE_EXTENSION_INIT3 /*no-op*/
#endif
#endif /* SQLITE3EXT_H */
+7
View File
@@ -0,0 +1,7 @@
{
"dag-engine": {
"enabled": false,
"issue": "0007",
"description": "Sistema propio de orquestacion de DAGs para reemplazar Dagu. Incluye parser YAML, executor con paralelismo, process manager, execution store y scheduler cron."
}
}
+137
View File
@@ -0,0 +1,137 @@
# 0007a — Funciones core del DAG engine
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0007a |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature |
## Dependencias
| ID | Título | Estado | Requerido |
|----|--------|--------|-----------|
| — | Ninguna | — | — |
**Bloqueada por:** ninguna
**Desbloquea:** `#0007b, #0007c, #0007d, #0007e`
---
## Objetivo
Crear las funciones puras que parsean, validan y ordenan DAGs definidos en YAML. Estas funciones son el nucleo del sistema de orquestacion — todo lo demas depende de ellas.
## Contexto
- Dagu usa YAML con `steps`, `depends`, `env`, `schedule` — queremos compatibilidad con ese formato
- Las funciones deben ser puras: reciben datos, retornan datos, sin I/O
- Deben vivir en `functions/core/` (Go) para maxima composabilidad
- El formato YAML de Dagu existente en `~/dagu/dags/` debe poder parsearse sin cambios
## Arquitectura
```
functions/core/
├── dag_parse.go — NEW: YAML → DagDefinition
├── dag_parse.md — NEW: metadata
├── dag_validate.go — NEW: valida ciclos, refs rotas, campos requeridos
├── dag_validate.md — NEW: metadata
├── dag_topo_sort.go — NEW: ordena steps por dependencias (Kahn's algorithm)
├── dag_topo_sort.md — NEW: metadata
├── dag_resolve_env.go — NEW: sustituye variables ${VAR} en steps
├── dag_resolve_env.md — NEW: metadata
types/core/
├── dag_definition.md — NEW: tipo DagDefinition (product)
├── dag_step.md — NEW: tipo DagStep (product)
├── dag_schedule.md — NEW: tipo DagSchedule (product)
├── dag_result.md — NEW: tipo DagValidationResult (product)
```
### Patron pure core / impure shell
- `core/` — Todas las funciones de este issue son puras
- No hay shell/impure en este issue
- Los tipos usan nativos de Go en firmas, tipos del registry en `uses_types`
## Tareas
### Fase 1: Tipos
- [ ] **1.1** Definir `DagStep` — name, command, args, depends, env, timeout, retry, tags
- [ ] **1.2** Definir `DagSchedule` — cron expressions, timezone
- [ ] **1.3** Definir `DagDefinition` — name, description, steps, env, schedule, tags
- [ ] **1.4** Definir `DagValidationResult` — errors, warnings, step_order
### Fase 2: Parser
- [ ] **2.1** `dag_parse` — YAML bytes → DagDefinition. Soportar formato Dagu: steps con command/depends/env
- [ ] **2.2** Tests: parsear DAGs existentes de `~/dagu/dags/`, edge cases (YAML invalido, campos faltantes)
### Fase 3: Validacion
- [ ] **3.1** `dag_validate` — detectar ciclos (DFS), referencias rotas en depends, steps sin nombre, nombres duplicados
- [ ] **3.2** Tests: grafos ciclicos, DAGs validos, depends a steps inexistentes
### Fase 4: Topological sort
- [ ] **4.1** `dag_topo_sort` — Kahn's algorithm, retorna steps en orden de ejecucion con niveles de paralelismo
- [ ] **4.2** Tests: DAGs lineales, DAGs con ramas paralelas, diamond dependencies
### Fase 5: Resolucion de env
- [ ] **5.1** `dag_resolve_env` — sustituye `${VAR}` y `$VAR` en command/args de cada step usando env del DAG + env del step
- [ ] **5.2** Tests: variables anidadas, variables no definidas, escaping
### Fase 6: Cleanup
- [ ] `fn index` y verificar todos los IDs
- [ ] Verificar que todos los tipos son referenciados correctamente en uses_types
---
## Ejemplo de uso
```go
// Parsear un DAG
data, _ := os.ReadFile("dags/my_pipeline.yaml")
dag, err := dag_parse(data)
// Validar
result := dag_validate(dag)
if len(result.Errors) > 0 {
// ciclos, refs rotas...
}
// Ordenar
ordered := dag_topo_sort(dag.Steps)
// ordered = [[step_a], [step_b, step_c], [step_d]]
// nivel 0 nivel 1 (paralelo) nivel 2
// Resolver env
resolved := dag_resolve_env(dag, os.Environ())
```
## Decisiones de diseno
- **Kahn's algorithm sobre DFS topo sort**: Kahn's da niveles de paralelismo gratis — steps en el mismo nivel pueden ejecutarse en paralelo
- **Formato Dagu compatible**: no inventar formato nuevo, reutilizar el YAML que ya existe
- **Tipos nativos en firma**: `[]byte` entrada, structs con campos basicos, sin dependencias externas para parsear
## Criterios de aceptacion
- [ ] `dag_parse` parsea correctamente los DAGs existentes en `~/dagu/dags/`
- [ ] `dag_validate` detecta ciclos y referencias rotas
- [ ] `dag_topo_sort` retorna orden correcto con niveles de paralelismo
- [ ] Todas las funciones son puras (sin I/O, sin estado)
- [ ] Tests pasan con `go test -tags fts5 ./...`
- [ ] Indexado en registry.db
## Referencias
- Dagu YAML spec: `~/dagu/dags/example.yaml`
- Kahn's algorithm: topological sort con BFS que da niveles
+115
View File
@@ -0,0 +1,115 @@
# 0007b — Process manager: spawn, wait, kill, status
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0007b |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature |
## Dependencias
| ID | Título | Estado | Requerido |
|----|--------|--------|-----------|
| 0007a | Funciones core del DAG engine | pendiente | Si |
**Bloqueada por:** `#0007a`
**Desbloquea:** `#0007e`
---
## Objetivo
Funciones impuras para gestionar procesos hijo: lanzar, esperar, matar, consultar estado. Son los bloques que el executor usara para correr cada step de un DAG.
## Contexto
- Cada step de un DAG se ejecuta como un proceso hijo (`os/exec`)
- Necesitamos captura de stdout/stderr, timeout, señales (SIGTERM/SIGKILL)
- Deben ser funciones atomicas — el executor las compone
- Dominio `infra` porque gestionan recursos del sistema
## Arquitectura
```
functions/infra/
├── process_spawn.go — NEW: lanza proceso, retorna PID + pipes
├── process_spawn.md
├── process_wait.go — NEW: espera proceso con timeout
├── process_wait.md
├── process_kill.go — NEW: envia señal a proceso (SIGTERM, SIGKILL)
├── process_kill.md
├── process_status.go — NEW: consulta estado de PID (running, exited, code)
├── process_status.md
types/infra/
├── process_handle.md — NEW: PID, stdin/stdout/stderr pipes, start_time
├── process_result.md — NEW: exit_code, stdout, stderr, duration_ms
```
### Patron pure core / impure shell
- `core/` — No aplica en este issue
- `infra/` — Todas impuras (spawn procesos, I/O con OS)
- `error_type`: `error_go_core` para todas
## Tareas
### Fase 1: Tipos
- [ ] **1.1** Definir `ProcessHandle` — pid, cmd, start_time, working_dir
- [ ] **1.2** Definir `ProcessResult` — exit_code, stdout, stderr, duration_ms, killed
### Fase 2: Funciones
- [ ] **2.1** `process_spawn` — ejecuta comando con args, env, working_dir. Retorna ProcessHandle. No bloquea.
- [ ] **2.2** `process_wait` — espera a que el proceso termine o timeout. Retorna ProcessResult.
- [ ] **2.3** `process_kill` — envia SIGTERM, espera grace period, luego SIGKILL si sigue vivo
- [ ] **2.4** `process_status` — consulta si el PID sigue corriendo, retorna estado
### Fase 3: Tests
- [ ] **3.1** Tests: spawn+wait de `echo hello`, timeout con `sleep 999`, kill de proceso largo
- [ ] **3.2** Tests: captura correcta de stdout/stderr, exit codes no-zero
### Fase 4: Cleanup
- [ ] `fn index` y verificar IDs
- [ ] Verificar error_type en todas las funciones impuras
---
## Ejemplo de uso
```go
handle, err := process_spawn(ProcessSpawnInput{
Command: "python3",
Args: []string{"script.py", "--flag"},
Env: []string{"API_KEY=xxx"},
WorkingDir: "/home/lucas/project",
})
result, err := process_wait(handle, 30*time.Second) // timeout 30s
// result.ExitCode == 0, result.Stdout == "output..."
// O matar si tarda demasiado
process_kill(handle, 5*time.Second) // SIGTERM, 5s grace, luego SIGKILL
```
## Decisiones de diseno
- **Spawn no bloquea**: retorna handle inmediatamente, wait es separado — permite al executor lanzar steps en paralelo
- **Kill con grace period**: SIGTERM primero, espera, SIGKILL si no murio — comportamiento estandar de process managers
- **Stdout/stderr como strings**: para steps cortos. Para steps con output grande, futuro: streaming a archivo
## Criterios de aceptacion
- [ ] Spawn y wait funcionan con comandos reales
- [ ] Timeout mata el proceso correctamente
- [ ] Kill con grace period funciona
- [ ] Exit codes se capturan correctamente
- [ ] Tests pasan
- [ ] Indexado en registry.db
+115
View File
@@ -0,0 +1,115 @@
# 0007c — Execution store: persistencia de estado
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0007c |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature |
## Dependencias
| ID | Título | Estado | Requerido |
|----|--------|--------|-----------|
| 0007a | Funciones core del DAG engine | pendiente | Si |
**Bloqueada por:** `#0007a`
**Desbloquea:** `#0007e`
---
## Objetivo
Funciones para persistir el estado de ejecuciones de DAGs en SQLite: que DAG se ejecuto, cuando, que steps corrieron, resultado de cada step, logs. Permite historial, reintentos y debugging.
## Contexto
- Cada ejecucion de un DAG genera un `run` con multiples `step_results`
- Similar a `operations.db` pero especifico para el DAG engine
- La BD vive en el directorio de la app del scheduler (no en raiz)
- Debe soportar consultas tipo: "ultimas 10 ejecuciones de X", "steps fallidos"
## Arquitectura
```
functions/infra/
├── dag_store_init.go — NEW: crea schema SQLite para runs/steps
├── dag_store_init.md
├── dag_store_run.go — NEW: CRUD de runs (create, update status, list, get)
├── dag_store_run.md
├── dag_store_step.go — NEW: CRUD de step results dentro de un run
├── dag_store_step.md
types/infra/
├── dag_run.md — NEW: id, dag_name, status, started_at, finished_at, trigger
├── dag_step_result.md — NEW: run_id, step_name, status, exit_code, stdout, stderr, duration_ms
```
### Patron pure core / impure shell
- `infra/` — Todas impuras (SQLite I/O)
- Schema sencillo: `dag_runs` + `dag_step_results`
## Tareas
### Fase 1: Tipos y schema
- [ ] **1.1** Definir `DagRun` — id (ULID), dag_name, dag_path, status (pending/running/success/failed/cancelled), started_at, finished_at, trigger (manual/schedule/api)
- [ ] **1.2** Definir `DagStepResult` — id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms
- [ ] **1.3** Schema SQLite: `dag_runs`, `dag_step_results` con indices
### Fase 2: Funciones
- [ ] **2.1** `dag_store_init` — crea/migra la BD SQLite
- [ ] **2.2** `dag_store_run` — create_run, update_run_status, get_run, list_runs (con filtros)
- [ ] **2.3** `dag_store_step` — insert_step_result, list_steps_for_run
### Fase 3: Tests
- [ ] **3.1** Ciclo completo: init → create run → insert steps → update status → query
- [ ] **3.2** Queries: ultimas N ejecuciones, ejecuciones fallidas, steps de un run
### Fase 4: Cleanup
- [ ] `fn index` y verificar IDs
---
## Ejemplo de uso
```go
db := dag_store_init("/path/to/scheduler.db")
run := dag_store_create_run(db, "my_pipeline", "manual")
// run.ID = "01HXZ..."
dag_store_insert_step(db, run.ID, DagStepResult{
StepName: "fetch_data",
Status: "success",
ExitCode: 0,
Stdout: "fetched 1000 rows",
DurationMs: 1234,
})
dag_store_update_status(db, run.ID, "success")
// Consultar
runs := dag_store_list_runs(db, "my_pipeline", 10) // ultimas 10
```
## Decisiones de diseno
- **SQLite por DAG engine, no por DAG**: una sola BD para todas las ejecuciones, no una por cada DAG
- **ULID para run IDs**: ordenables por tiempo, unicos sin coordinacion
- **stdout/stderr en BD**: para steps cortos. Para output grande, guardar path a archivo de log
## Criterios de aceptacion
- [ ] Schema se crea correctamente
- [ ] CRUD completo funciona
- [ ] Queries con filtros funcionan
- [ ] Tests pasan
- [ ] Indexado en registry.db
+109
View File
@@ -0,0 +1,109 @@
# 0007d — Scheduler: cron parser y ticker
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0007d |
| **Estado** | pendiente |
| **Prioridad** | media |
| **Tipo** | feature |
## Dependencias
| ID | Título | Estado | Requerido |
|----|--------|--------|-----------|
| 0007a | Funciones core del DAG engine | pendiente | Si |
| 0007c | Execution store | pendiente | Si |
**Bloqueada por:** `#0007a, #0007c`
**Desbloquea:** `#0007e`
---
## Objetivo
Funciones para parsear expresiones cron, calcular proximas ejecuciones, y un ticker que dispara DAGs segun su schedule. Es lo que reemplaza el scheduler de Dagu.
## Contexto
- Las expresiones cron de Dagu son estandar (5 campos: min hour dom mon dow)
- El ticker es un loop infinito que cada minuto evalua que DAGs deben lanzarse
- Funciones puras para parseo y calculo, impura solo el ticker
## Arquitectura
```
functions/core/
├── cron_parse.go — NEW: string → CronExpression
├── cron_parse.md
├── cron_next.go — NEW: CronExpression + time → proxima ejecucion
├── cron_next.md
├── cron_match.go — NEW: CronExpression + time → bool (coincide?)
├── cron_match.md
functions/infra/
├── dag_ticker.go — NEW: loop que evalua schedules y lanza DAGs
├── dag_ticker.md
types/core/
├── cron_expression.md — NEW: minute, hour, dom, month, dow (cada uno []int o wildcard)
```
### Patron pure core / impure shell
- `core/``cron_parse`, `cron_next`, `cron_match` son puras
- `infra/``dag_ticker` es impuro (time.Sleep, lanza ejecuciones)
## Tareas
### Fase 1: Tipos
- [ ] **1.1** Definir `CronExpression` — campos parseados con soporte para *, ranges (1-5), lists (1,3,5), intervals (*/5)
### Fase 2: Funciones puras
- [ ] **2.1** `cron_parse` — "0 9 * * *" → CronExpression. Soportar: *, N, N-M, N/M, listas
- [ ] **2.2** `cron_next` — dada una CronExpression y un time.Time, retorna el proximo time.Time que coincide
- [ ] **2.3** `cron_match` — dada una CronExpression y un time.Time, retorna true si coincide (para el ticker)
- [ ] **2.4** Tests exhaustivos: wildcards, ranges, listas, intervalos, edge cases (fin de mes, febrero)
### Fase 3: Ticker
- [ ] **3.1** `dag_ticker` — recibe lista de (DagDefinition, path), cada minuto evalua cron_match para cada uno, lanza los que coinciden
- [ ] **3.2** Soporte para cancelacion (context.Context) y graceful shutdown
### Fase 4: Cleanup
- [ ] `fn index` y verificar IDs
---
## Ejemplo de uso
```go
// Puro
expr, _ := cron_parse("*/5 9-17 * * 1-5") // cada 5 min, 9-17h, lun-vie
next := cron_next(expr, time.Now()) // proxima ejecucion
matches := cron_match(expr, time.Now()) // true si ahora coincide
// Impuro (el ticker)
ctx, cancel := context.WithCancel(context.Background())
dag_ticker(ctx, dags, executor) // loop infinito hasta cancel
```
## Decisiones de diseno
- **No usar libreria cron externa**: las expresiones son simples, implementar desde cero es ~100 lineas y evita dependencias
- **Separar parse/next/match**: parse es costoso, match es barato — parsear una vez, match cada minuto
- **Ticker como funcion, no como goroutine**: el caller decide como lanzarlo
## Criterios de aceptacion
- [ ] Parsea todas las expresiones cron de los DAGs existentes en `~/dagu/dags/`
- [ ] `cron_next` calcula correctamente la proxima ejecucion
- [ ] `cron_match` coincide correctamente para el minuto actual
- [ ] Ticker lanza DAGs en el momento correcto
- [ ] Tests pasan
- [ ] Indexado en registry.db
+143
View File
@@ -0,0 +1,143 @@
# 0007e — DAG executor app: CLI/TUI que reemplaza Dagu
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0007e |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature |
## Dependencias
| ID | Título | Estado | Requerido |
|----|--------|--------|-----------|
| 0007a | Funciones core del DAG engine | pendiente | Si |
| 0007b | Process manager | pendiente | Si |
| 0007c | Execution store | pendiente | Si |
| 0007d | Scheduler | pendiente | Si |
**Bloqueada por:** `#0007a, #0007b, #0007c, #0007d`
**Desbloquea:** ninguna
---
## Objetivo
App que compone todas las funciones de 0007a-d en un ejecutable unico que reemplaza a Dagu: lee DAGs YAML, los ejecuta con dependencias, persiste estado, y opcionalmente corre como daemon con scheduler.
## Contexto
- Vive en `apps/dag_engine/` (es una app, no una funcion reutilizable)
- Lee DAGs del directorio `~/dagu/dags/` (o configurable)
- El executor es el nucleo: toma un DagDefinition, lanza steps en orden topologico, gestiona paralelismo
- Modos: `run` (ejecuta un DAG), `start` (daemon con scheduler), `status` (consulta ejecuciones), `list` (lista DAGs)
## Arquitectura
```
apps/dag_engine/
├── app.md — metadata del registry
├── main.go — CLI: subcomandos run/start/status/list
├── executor.go — compone dag_topo_sort + process_spawn/wait + store
├── server.go — (futuro) HTTP API para trigger remoto
├── go.mod
├── .gitignore
```
### Patron pure core / impure shell
- `core/` — ya creadas en 0007a y 0007d (funciones puras del registry)
- `infra/` — ya creadas en 0007b y 0007c (funciones impuras del registry)
- `app/``executor.go` compone todo, `main.go` orquesta
## Tareas
### Fase 1: Executor
- [ ] **1.1** `executor.go` — funcion `ExecuteDAG(dag DagDefinition, store DB) DagRun`
- Crea run en store
- Resuelve env
- Ordena steps (topo sort)
- Ejecuta nivel por nivel: steps del mismo nivel van en paralelo (goroutines)
- Cada step: spawn → wait → guarda result en store
- Si un step falla: cancela dependientes, marca run como failed
- Retorna DagRun con resultado final
### Fase 2: CLI
- [ ] **2.1** `fn-dag run <path.yaml>` — parsea, valida, ejecuta, muestra resultado
- [ ] **2.2** `fn-dag list [dir]` — lista DAGs con su schedule y ultimo estado
- [ ] **2.3** `fn-dag status [dag_name]` — ultimas ejecuciones, detalle de steps
- [ ] **2.4** `fn-dag start [dir]` — daemon: carga todos los DAGs, arranca ticker
### Fase 3: Integracion
- [ ] **3.1** `app.md` con uses_functions referenciando todas las funciones de 0007a-d
- [ ] **3.2** `operations.db` inicializado (fn ops init)
- [ ] **3.3** Publicar en Gitea (dataforge/dag_engine)
### Fase 4: Tests e2e
- [ ] **4.1** Ejecutar DAGs existentes de `~/dagu/dags/` y comparar resultado con Dagu
- [ ] **4.2** Test: DAG con steps paralelos, DAG con fallo en medio, DAG con timeout
### Fase 5: Cleanup
- [ ] `fn index`
- [ ] Actualizar CLAUDE.md con documentacion del dag engine
---
## Ejemplo de uso
```bash
# Ejecutar un DAG
fn-dag run ~/dagu/dags/example.yaml
# Step hello... done (0.1s)
# Step list_files... done (0.2s)
# Step date... done (0.1s)
# Run completed: 3/3 steps succeeded (0.4s)
# Listar DAGs
fn-dag list ~/dagu/dags/
# NAME SCHEDULE LAST RUN STATUS
# example 0 9 * * * 2026-04-07 success
# example_lineage_tracking 0 */6 * * * 2026-04-08 failed
# Ver estado
fn-dag status example
# RUN_ID STARTED STATUS STEPS
# 01HXZ... 2026-04-08 09:00:01 success 3/3
# 01HXY... 2026-04-07 09:00:00 success 3/3
# Daemon con scheduler
fn-dag start ~/dagu/dags/
# [09:00] Scheduler started. Watching 5 DAGs.
# [09:00] Triggered: example (schedule match)
# ...
```
## Decisiones de diseno
- **Un binario, no un servicio**: `fn-dag run` es fire-and-forget. `fn-dag start` es el unico modo daemon.
- **Paralelismo por niveles**: steps en el mismo nivel topologico corren en goroutines, no hay limite de concurrencia (por ahora)
- **Compatible con DAGs de Dagu**: lee el mismo formato YAML, no requiere migracion
- **Sin web UI por ahora**: la TUI y/o web UI es un issue futuro, el CLI cubre el 80% del uso
## Riesgos
- **Riesgo**: DAGs complejos de Dagu usan features que no implementamos (preconditions, params, mail on failure). **Mitigacion**: empezar con el subset que usamos, documentar que no se soporta.
- **Riesgo**: Race conditions en el executor paralelo. **Mitigacion**: cada goroutine tiene su propio ProcessHandle, el store usa transacciones SQLite.
## Criterios de aceptacion
- [ ] `fn-dag run` ejecuta correctamente los DAGs existentes
- [ ] Steps paralelos se ejecutan concurrentemente
- [ ] Fallos en un step cancelan dependientes
- [ ] Estado se persiste en SQLite
- [ ] `fn-dag start` corre como daemon con scheduler
- [ ] App registrada en registry.db e indexada
- [ ] Publicada en Gitea
+183
View File
@@ -0,0 +1,183 @@
# 0008 — SQLite API Web
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0008 |
| **Estado** | 🟡 pendiente |
| **Prioridad** | alta |
| **Tipo** | feature |
## Dependencias
Ninguna.
---
## Objetivo
App que expone `registry.db` y los `operations.db` de cada app como API REST HTTP, permitiendo que herramientas externas (dashboards, scripts, agentes, frontends) consulten las bases de datos del registry sin necesidad de acceso directo al filesystem ni SQLite CLI.
## Contexto
- Actualmente para consultar `registry.db` hay que estar en la misma máquina y usar `sqlite3` directamente o funciones Go que abren el archivo.
- Las apps existentes (metabase_registry, registry_dashboard) acceden a SQLite localmente. Cualquier herramienta nueva que necesite datos del registry tiene que reimplementar la conexión.
- Con una API web, cualquier cliente HTTP (curl, fetch, Python requests, frontends React) puede consultar el registry de forma uniforme.
- Metabase ya resuelve visualización, pero no da acceso programático limpio a los datos para agentes y scripts remotos.
## Arquitectura
```
apps/sqlite_api/
├── main.go — NEW: Entry point, configura rutas y arranca servidor
├── handlers.go — NEW: Handlers HTTP (query, tables, schema)
├── config.go — NEW: Configuración (puerto, DBs permitidas, read-only)
├── app.md — NEW: Metadata de la app (tag: service)
└── operations.db — Runtime: operaciones propias
```
### Patrón pure core / impure shell
- **Funciones del registry usadas:** `http_get_json_go_infra`, `http_post_json_go_infra` (para tests/clientes), `cache_to_sqlite_go_infra` (opcional para cache de queries)
- **Core puro:** validación de queries (solo SELECT/PRAGMA permitidos), parsing de parámetros, formateo de resultados JSON
- **Shell impuro:** servidor HTTP, apertura de SQLite, ejecución de queries
## Diseño de API
### Endpoints
```
GET /api/databases — Lista de DBs disponibles
GET /api/databases/:db/tables — Lista tablas de una DB
GET /api/databases/:db/schema — Schema completo (.schema)
POST /api/databases/:db/query — Ejecuta query SQL (solo SELECT)
GET /api/databases/:db/fts?q=texto&table=functions — Búsqueda FTS5 directa
GET /health — Health check
```
### Bases de datos expuestas
| Alias | Path real | Descripción |
|-------|-----------|-------------|
| `registry` | `registry.db` (raíz) | Funciones, tipos, proposals |
| `ops:{app}` | `apps/{app}/operations.db` | Entities, relations, executions de cada app |
### Seguridad
- **Read-only obligatorio:** Solo queries SELECT y PRAGMA. Cualquier INSERT/UPDATE/DELETE/DROP se rechaza antes de ejecutar.
- **Bind por defecto a localhost** (`127.0.0.1:8484`). Flag `--bind` para cambiar.
- **Sin autenticación** en v1 (solo acceso local). Documentar cómo poner detrás de reverse proxy si se necesita auth.
- **Query timeout:** máximo 5 segundos por query para evitar bloqueos.
- **Apertura con `?mode=ro`** en el connection string de SQLite para doble protección.
### Formato de respuesta
```json
// POST /api/databases/registry/query
// Body: {"sql": "SELECT id, name, purity FROM functions WHERE domain = 'core' LIMIT 5"}
{
"columns": ["id", "name", "purity"],
"rows": [
["filter_slice_go_core", "filter_slice", "pure"],
["map_slice_go_core", "map_slice", "pure"]
],
"count": 2,
"duration_ms": 3
}
```
## Tareas
### Fase 1: Servidor base
- [ ] **1.1** Crear `apps/sqlite_api/` con `main.go`, `go.mod` (o usar módulo raíz)
- [ ] **1.2** Handler `/health` y `/api/databases` (lista estática de DBs detectadas)
- [ ] **1.3** Handler `POST /api/databases/:db/query` con validación read-only
- [ ] **1.4** Abrir DBs con `?mode=ro` y `-tags fts5`
- [ ] **1.5** `app.md` con tag `service`, documentar puerto y health check
### Fase 2: Endpoints de exploración
- [ ] **2.1** Handler `/api/databases/:db/tables` (lista tablas vía `sqlite_master`)
- [ ] **2.2** Handler `/api/databases/:db/schema` (output de `.schema`)
- [ ] **2.3** Handler `/api/databases/:db/fts` para búsqueda FTS5 sin escribir SQL
### Fase 3: Operations discovery
- [ ] **3.1** Auto-detectar `apps/*/operations.db` al arrancar
- [ ] **3.2** Exponer cada operations.db como `ops:{app_name}`
- [ ] **3.3** Endpoint `GET /api/databases` incluye las operations detectadas
### Fase 4: Cleanup y docs
- [ ] Crear `app.md` completo
- [ ] Ejecutar `go vet` y `go test`
- [ ] Actualizar issue en `dev/issues/README.md`
---
## Ejemplo de uso
```bash
# Arrancar el servicio
cd apps/sqlite_api && go run . --port 8484
# Health check
curl http://localhost:8484/health
# Listar databases disponibles
curl http://localhost:8484/api/databases
# Query al registry
curl -X POST http://localhost:8484/api/databases/registry/query \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, purity, description FROM functions WHERE domain = '\''core'\'' LIMIT 5"}'
# Búsqueda FTS5
curl "http://localhost:8484/api/databases/registry/fts?q=slice&table=functions"
# Schema
curl http://localhost:8484/api/databases/registry/schema
# Query a operations de una app
curl -X POST http://localhost:8484/api/databases/ops:pipeline_launcher/query \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT * FROM executions ORDER BY started_at DESC LIMIT 10"}'
```
```python
# Desde Python
import requests
r = requests.post("http://localhost:8484/api/databases/registry/query", json={
"sql": "SELECT id, name FROM functions WHERE purity = 'pure' AND domain = 'core'"
})
data = r.json()
for row in data["rows"]:
print(row[0], row[1])
```
## Decisiones de diseño
- **Go con net/http estándar**: sin framework externo, coherente con el resto del registry. Router simple con `http.ServeMux`.
- **Puerto 8484**: no colisiona con Metabase (3000), Jupyter (8888), ni otros servicios comunes.
- **Read-only estricto**: la API nunca modifica datos. Para escribir se usan los mecanismos existentes (`fn ops`, `fn index`).
- **Sin ORM**: queries se pasan tal cual a SQLite. El valor es el acceso HTTP, no una capa de abstracción SQL.
- **Auto-discovery de operations.db**: escanea `apps/*/operations.db` al inicio para no tener que configurar cada app manualmente.
## Riesgos
- **SQL injection vía queries arbitrarias**: Mitigado con apertura read-only (`?mode=ro`) + validación de que el statement empieza con SELECT o PRAGMA.
- **Queries pesadas bloquean el servidor**: Mitigado con timeout de 5s por query y context cancelable.
- **Archivos SQLite bloqueados por escritores concurrentes**: Mitigado con `journal_mode=wal` y apertura read-only que no bloquea escritores.
## Criterios de aceptación
- [ ] `curl localhost:8484/health` retorna 200
- [ ] Queries SELECT funcionan contra registry.db
- [ ] Queries INSERT/UPDATE/DELETE son rechazadas con 400
- [ ] Operations.db de apps existentes son accesibles como `ops:{nombre}`
- [ ] FTS5 funciona a través de la API
- [ ] Tag `service` en app.md
- [ ] El servidor arranca con `go run .` sin configuración adicional
+16
View File
@@ -0,0 +1,16 @@
# Issues
| ID | Título | Estado | Prioridad | Tipo | Bloquea |
|----|--------|--------|-----------|------|---------|
| 0001 | Jupyter create notebook | completado | — | feature | — |
| 0002 | Jupyter discover root dir | completado | — | bugfix | — |
| 0003 | Jupyter tools documentation | completado | — | docs | — |
| 0004 | Jupyter discover multiple instances | completado | — | feature | — |
| 0005 | Jupyter write batch | completado | — | feature | — |
| 0006 | Jupyter exec outputs keyerror | completado | — | bugfix | — |
| **0007a** | **DAG engine: core (parse, validate, topo sort)** | pendiente | alta | feature | 0007b-e |
| **0007b** | **DAG engine: process manager (spawn, wait, kill)** | pendiente | alta | feature | 0007e |
| **0007c** | **DAG engine: execution store (SQLite)** | pendiente | alta | feature | 0007e |
| **0007d** | **DAG engine: scheduler (cron parser, ticker)** | pendiente | media | feature | 0007e |
| **0007e** | **DAG engine: app CLI que reemplaza Dagu** | pendiente | alta | feature | — |
| **0008** | **SQLite API Web** | pendiente | alta | feature | — |
+40 -3
View File
@@ -1,6 +1,6 @@
[Window][WindowOverViewport_11111111] [Window][WindowOverViewport_11111111]
Pos=0,0 Pos=0,0
Size=1400,900 Size=1600,968
Collapsed=0 Collapsed=0
[Window][Debug##Default] [Window][Debug##Default]
@@ -13,6 +13,43 @@ Pos=45,133
Size=1260,514 Size=1260,514
Collapsed=0 Collapsed=0
[Docking][Data] [Window][fn_registry Dashboard]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,0 Size=1400,900 CentralNode=1 Selected=0x2DADAD08 Pos=84,64
Size=1440,871
Collapsed=0
[Table][0x3D1838B3,7]
RefScale=13
Column 0 Width=126 Sort=0v
Column 1 Width=40
Column 2 Width=54
Column 3 Width=63
Column 4 Width=54
Column 5 Width=54
Column 6 Width=70
[Table][0x736EAF20,5]
RefScale=13
Column 0 Width=119 Sort=0v
Column 1 Width=40
Column 2 Width=63
Column 3 Width=75
Column 4 Width=1002
[Table][0xC80217F1,3]
RefScale=13
Column 0 Width=133 Sort=0v
Column 1 Width=54
Column 2 Width=477
[Table][0x393CD6C2,5]
RefScale=13
Column 0 Width=182 Sort=0v
Column 1 Width=40
Column 2 Width=91
Column 3 Width=75
Column 4 Width=2710
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,0 Size=1600,968 CentralNode=1 Selected=0x22F01560
@@ -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. - `jupyter_kernel_execute` es sincrona directamente porque `KernelClient.execute` es bloqueante.
- El token puede ser cadena vacia si el servidor tiene autenticacion deshabilitada. - 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. - `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. - **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 import json
from functools import partial from functools import partial
from typing import Any from typing import Any
from urllib.error import URLError from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from jupyter_kernel_client import KernelClient 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: def _api_get(url: str, token: str = "") -> dict | list | None:
"""GET a Jupyter REST API endpoint.""" """GET a Jupyter REST API endpoint."""
headers = {"Accept": "application/json"} headers = {"Accept": "application/json"}
@@ -112,13 +186,14 @@ async def _async_append_execute(
server_url: str, server_url: str,
token: str, token: str,
) -> dict[str, Any]: ) -> 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( ws_url = get_jupyter_notebook_websocket_url(
server_url, server_url,
notebook_path, notebook_path,
token or None, token or None,
) )
kernel_id = _resolve_kernel_id(server_url, token, notebook_path)
username = _resolve_collab_username(server_url, token) username = _resolve_collab_username(server_url, token)
async with NbModelClient(ws_url, username=username) as nb: async with NbModelClient(ws_url, username=username) as nb:
@@ -149,12 +224,13 @@ async def _async_execute_cell(
server_url: str, server_url: str,
token: str, token: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
kernel_id = _ensure_session(server_url, token, notebook_path)
ws_url = get_jupyter_notebook_websocket_url( ws_url = get_jupyter_notebook_websocket_url(
server_url, server_url,
notebook_path, notebook_path,
token or None, token or None,
) )
kernel_id = _resolve_kernel_id(server_url, token, notebook_path)
username = _resolve_collab_username(server_url, token) username = _resolve_collab_username(server_url, token)
async with NbModelClient(ws_url, username=username) as nb: async with NbModelClient(ws_url, username=username) as nb:
+10 -2
View File
@@ -6,8 +6,8 @@ domain: notebook
version: "1.0.0" version: "1.0.0"
purity: impure purity: impure
signature: "def jupyter_kernel_list(server_url: str = \"http://localhost:8888\", token: str = \"\") -> list[dict]" 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." 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] tags: [jupyter, notebook, kernel, api, http, rest, sessions, crud, cleanup]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] 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_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_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_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 ## Ejemplo
@@ -88,6 +90,12 @@ python python/functions/notebook/jupyter_kernel.py shutdown abc123-...
# Listar sesiones # Listar sesiones
python python/functions/notebook/jupyter_kernel.py sessions 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. Todos los subcomandos aceptan `--server` y `--token`. El output es siempre JSON.
@@ -196,6 +196,80 @@ def jupyter_kernel_sessions(
return 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 # CLI
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -248,6 +322,18 @@ if __name__ == "__main__":
# sessions # sessions
subparsers.add_parser("sessions", help="Lista las sesiones activas.") 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() args = parser.parse_args()
try: try:
@@ -267,6 +353,10 @@ if __name__ == "__main__":
result = {"status": "shutdown", "kernel_id": args.kernel_id} result = {"status": "shutdown", "kernel_id": args.kernel_id}
elif args.command == "sessions": elif args.command == "sessions":
result = jupyter_kernel_sessions(args.server, args.token) 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: else:
parser.print_help() parser.print_help()
sys.exit(1) 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`. - 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). - `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. - 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.

Some files were not shown because too many files have changed in this diff Show More