Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11e6e27ad1 | |||
| a59b12d467 | |||
| fe4320af89 | |||
| f71e0f4c9a | |||
| 46b4385331 | |||
| 580238b32e | |||
| ed767360c1 | |||
| 5bac05ce13 | |||
| d0ceea6f3d | |||
| 0f905b78e0 | |||
| 5c7ff8d761 | |||
| 138f4b2713 | |||
| 25425a5fd6 | |||
| 89441539fa | |||
| 1d3d2f43b3 | |||
| 2effb688b0 | |||
| eb30074792 | |||
| f8efb7d177 | |||
| f428f2c82a | |||
| f36d091704 | |||
| 938853d268 | |||
| b31ea70771 | |||
| 2e5c630d38 | |||
| c52846d475 | |||
| b5affae68c | |||
| 5b4452b9fe | |||
| e0f8f3a068 | |||
| b21e7587ad | |||
| d4924f5cab | |||
| 853b3c0363 | |||
| f164ef230f | |||
| ff255c9a3c | |||
| 6c7f60fb6c | |||
| 75ac96a2d1 | |||
| da56085e74 | |||
| ecd864f2d3 | |||
| a91ef5aace | |||
| c2bdc586a4 | |||
| 61a46e4b21 | |||
| 3633d128aa | |||
| 892ff4f789 | |||
| 4388b54356 | |||
| b21adb40c9 | |||
| 6fd2e9d071 | |||
| d9ef4e54f4 | |||
| 2ea9206934 | |||
| 355bcac6c7 | |||
| 4eb4c1cf98 | |||
| 40aacac590 | |||
| e9bcbecd24 | |||
| 7eb7b3d0c8 | |||
| 61ec4c8a76 | |||
| a843f84a18 | |||
| 6f3c129a14 | |||
| bc270db723 | |||
| a3a263702b | |||
| 78c4f593a4 | |||
| 0f72cc8ad3 | |||
| 030e44b027 | |||
| ca2e5588cc | |||
| 5fb2269c00 | |||
| 5e6a974a5d | |||
| 5d2a14e50a | |||
| 212875ed0d | |||
| d6175964e4 | |||
| 5974484bd4 | |||
| 67fff0d677 | |||
| 890f641692 | |||
| ae5d27a5ec | |||
| 0ed949d235 | |||
| c438dc6916 | |||
| 4027aeaaf5 | |||
| 9ff0b3900c | |||
| ce9fa3b451 | |||
| e0cce972ea | |||
| 380a7a8f35 | |||
| 7ecbee1175 | |||
| fe39de8b22 | |||
| 951a77ec7f |
+4
-1
@@ -21,12 +21,14 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E
|
||||
|
||||
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
|
||||
|
||||
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
|
||||
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps/<name>/` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). Ver `.claude/rules/apps_subrepo.md`.
|
||||
|
||||
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
|
||||
|
||||
**Reglas y convenciones:** ver `.claude/rules/INDEX.md`
|
||||
|
||||
**Slash commands:** `/commands` lista todos los slash commands del repo agrupados por namespace (global + projects). Project commands viven en `projects/<p>/.claude/commands/` y se exponen como `/<project>:<cmd>` via symlink. Ver `.claude/rules/project_commands.md`.
|
||||
|
||||
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
|
||||
|
||||
---
|
||||
@@ -258,6 +260,7 @@ fn check params # Lista funciones sin params_schema
|
||||
fn doctor # Corre todos los checks
|
||||
fn doctor artefacts # git/venv/app.md/upstream de cada app y analysis
|
||||
fn doctor services # apps tag 'service' + systemctl + puerto
|
||||
fn doctor services-spec # audita bloque `service:` del app.md (issue 0105)
|
||||
fn doctor sync # drift pc_locations BD vs disco
|
||||
fn doctor uses-functions # imports reales vs uses_functions del app.md
|
||||
fn doctor unused # funciones del registry sin consumidores
|
||||
|
||||
@@ -30,6 +30,16 @@ Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks
|
||||
6. **Auditoria total**. Cada decision se loggea en `task_runs.progress_json` con razonamiento + fase + run_id.
|
||||
7. **No self-modify**. NO modificas tu propio SKILL.md ni el de otros subagentes en la misma run.
|
||||
8. **Cero produccion**. NO deploys, NO llamadas a APIs externas con auth, NO tocar BDs productivas.
|
||||
9. **NUNCA paths absolutos fuera del worktree**. SIEMPRE rutas relativas o absolutas que apunten dentro de `/tmp/fn_orq_<issue>_<ts>/`. Si necesitas leer algo del repo principal (ej. plantillas docs), copialo al worktree primero. Refuerzo del piloto 1 (2026-05-15): orquestador modifico hooks bash del repo principal usando paths absolutos `/home/lucas/fn_registry/bash/functions/...` para destrancar pre-commit. Solucion correcta: el fix vive en el worktree, NO en main.
|
||||
10. **Pre-commit hook compartido**. Worktrees comparten `.git/hooks/` con main repo. Si el hook llama scripts via path absoluto a main (ej. `/home/lucas/fn_registry/bash/functions/cybersecurity/scan_secrets_in_dirty.sh`), el hook ejecutara la version de MAIN, no la del worktree. Opciones legitimas:
|
||||
a. Aplicar el fix del hook EN EL WORKTREE y commitearlo en `auto/*` — al mergear el PR, main heredara el fix.
|
||||
b. Si el hook bloquea progreso y el fix del hook excede tu scope, `git commit --no-verify` para ESE commit SOLO, documentando excepcion en `task_runs.events_json[].decision="skip_hook"` con razon.
|
||||
NO modificar archivos en main directamente.
|
||||
11. **Post-iteracion sanity check**. Tras cada commit en `auto/*`, verificar:
|
||||
```bash
|
||||
git -C /home/lucas/fn_registry status --short
|
||||
```
|
||||
Si la salida cambia respecto al baseline (capturado al inicio del piloto), HAS contaminado el repo principal. ABORT con `status=sandbox_breach` y reporta los archivos afectados en el output al humano.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../projects/aurgi/.claude/commands
|
||||
@@ -1,121 +1,36 @@
|
||||
# /autonomous-task — Lanza fn-orquestador (Fase 6 del ciclo reactivo)
|
||||
|
||||
Lanza el meta-orquestador autonomo que recorre el bucle CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR sobre un issue, sin intervencion humana, hasta convergencia / estancamiento / timeout / limite de iteraciones.
|
||||
|
||||
Issue 0069. Pre-condiciones obligatorias (chequear ANTES de despachar):
|
||||
|
||||
1. Migration `fn_operations/migrations/006_task_runs.sql` aplicada.
|
||||
2. Subagentes `fn-constructor`, `fn-executor`, `fn-recopilador`, `fn-analizador`, `fn-mejorador`, `fn-orquestador` presentes en `.claude/agents/`.
|
||||
3. `dev/autonomous_protected_paths.json` existe.
|
||||
4. `master` local up-to-date con `origin/master`.
|
||||
5. Branch `auto/<issue_id>` NO existe ya.
|
||||
6. `gh auth status` OK (necesario para PR draft al converger).
|
||||
7. Tipo de tarea soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`.
|
||||
|
||||
Si alguna pre-condicion falla → ABORT con razon. NO improvisar.
|
||||
|
||||
---
|
||||
description: "DEPRECADO 2026-05-19 — usa /autopilot. Wrapper directo a fn-orquestador conservado solo como debug primitive."
|
||||
---
|
||||
|
||||
## Argumento
|
||||
# /autonomous-task — DEPRECADO (sustituido por `/autopilot`)
|
||||
|
||||
`$ARGUMENTS` — `<issue_id>` o `<task_spec_path>` + flags opcionales.
|
||||
**ESTADO:** deprecado 2026-05-19. Usa `/autopilot <NNNN>` en su lugar.
|
||||
|
||||
```
|
||||
/autonomous-task 0070
|
||||
/autonomous-task 0070 --max-iterations 15 --max-minutes 90
|
||||
/autonomous-task 0070 --auto-apply-proposals safe
|
||||
/autonomous-task 0070 --dry-run
|
||||
/autonomous-task path/to/spec.yaml --branch auto/custom-name
|
||||
```
|
||||
## Por que deprecado
|
||||
|
||||
Flags:
|
||||
- `--max-iterations N` tope de iteraciones (default 10)
|
||||
- `--max-minutes M` timeout total (default 60)
|
||||
- `--auto-apply-proposals` `none|safe|aggressive` (default `safe`)
|
||||
- `--branch NAME` rama TBD (default `auto/<issue_id>`)
|
||||
- `--dry-run` simula, NO aplica
|
||||
`/autopilot` (v2, 2026-05-19) absorbe la funcionalidad y anade:
|
||||
- Pre-flight DoD readiness check (gate STOP — no arranca sin DoD).
|
||||
- Detector issue vs flow.
|
||||
- Reporte estructurado al humano post-delegate.
|
||||
- Self-Q&A migrado a fn-orquestador.
|
||||
|
||||
---
|
||||
Behaviour orquestador-side es identico. La unica diferencia es que `/autopilot` valida antes de delegar; `/autonomous-task` delegaba ciego.
|
||||
|
||||
## Comportamiento
|
||||
## Sustitucion 1:1
|
||||
|
||||
1. **Verificar pre-condiciones** con script bash (ver arriba). Si alguna falla, reportar y salir.
|
||||
2. **Despachar a `fn-orquestador`** via Agent tool con `subagent_type=fn-orquestador`. Pasar:
|
||||
- `issue_id` o `task_spec`
|
||||
- flags resueltos
|
||||
- paths protegidos (leidos de `dev/autonomous_protected_paths.json`)
|
||||
3. **El subagente:**
|
||||
- Crea worktree aislado `/tmp/fn_orq_<issue>_<ts>/` desde `master`.
|
||||
- Persiste estado en `task_runs` (operations.db del app target o repo root).
|
||||
- Despacha por fases a los 5 subagentes especializados.
|
||||
- Aplica proposals filtradas por `--auto-apply-proposals`.
|
||||
- Termina con: `converged` (PR draft creado) | `stalled` | `timeout` | `iterations_exhausted` | `needs_human` | `aborted`.
|
||||
4. **Reportar resultado al humano** con:
|
||||
- `status`, `iterations / max`, `duration / max`
|
||||
- `branch`, `worktree`, `PR draft url` si converged
|
||||
- `proposals creadas / aplicadas`
|
||||
- `last run_id` y status
|
||||
- Resumen iter-por-iter del `progress_json`
|
||||
| Antes | Ahora |
|
||||
|---|---|
|
||||
| `/autonomous-task 0070` | `/autopilot 0070` |
|
||||
| `/autonomous-task 0070 --max-iterations 15 --max-minutes 90` | `/autopilot 0070 --max-iterations 15 --max-minutes 90` |
|
||||
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
|
||||
| `/autonomous-task 0070 --auto-apply-proposals safe` | `/autopilot 0070 --auto-apply-proposals safe` |
|
||||
|
||||
---
|
||||
## Modo debug
|
||||
|
||||
## Reglas duras (no negociables)
|
||||
Si `/autopilot` falla en pre-flight pero quieres forzar dispatch sin DoD check (debug / experimentos), puedes seguir usando `/autonomous-task` que va directo a `fn-orquestador` sin validar. NO RECOMENDADO para uso normal.
|
||||
|
||||
- Sandbox de rama EN WORKTREE — nunca toca master ni el working tree del humano.
|
||||
- No merge automatico — PR draft siempre.
|
||||
- No `--no-verify`, no `--force`, no skip hooks.
|
||||
- Paths protegidos via `dev/autonomous_protected_paths.json`.
|
||||
- Watchdog: 2 iteraciones con mismo set de fails → `status=stalled`.
|
||||
- Auditoria total en `task_runs.progress_json`.
|
||||
- No self-modification: NO toca `.claude/agents/` ni `.claude/commands/`.
|
||||
## Migration deadline
|
||||
|
||||
---
|
||||
Sin deadline duro — `/autonomous-task` seguira funcionando hasta que un commit lo elimine. Pero NO se anaden nuevas features aqui; cualquier mejora va a `/autopilot`.
|
||||
|
||||
## Integracion con call_monitor (issue 0085)
|
||||
|
||||
El orquestador puede leer `projects/fn_monitoring/apps/call_monitor/operations.db` para:
|
||||
|
||||
- Consultar `function_stats` antes de decidir que funciones usar/reusar.
|
||||
- Filtrar proposals existentes via `mcp__registry__fn_proposal --status pending` para evitar duplicados.
|
||||
- Loggear sus invocaciones via el hook PostToolUse (automatico).
|
||||
|
||||
Tras converger, el `call_monitor propose` ejecutado por el humano (o futuro cron) absorbera las nuevas violations / copied_code / fails para alimentar la siguiente ronda.
|
||||
|
||||
---
|
||||
|
||||
## Tipos NO soportados
|
||||
|
||||
- Diseño arquitectura nuevo (humano decide).
|
||||
- Decisiones UX subjetivas.
|
||||
- Cambios BD productiva.
|
||||
- Cualquier cosa que toque secrets/credenciales.
|
||||
- Self-modification del propio orquestador.
|
||||
|
||||
Si el issue contiene criterios no-verificables programaticamente, ABORT con `status=needs_human`.
|
||||
|
||||
---
|
||||
|
||||
## Output canonico
|
||||
|
||||
```
|
||||
=== /autonomous-task: 0070 ===
|
||||
status: converged
|
||||
iterations: 7 / 10
|
||||
duration: 23 min / 60
|
||||
branch: auto/0070
|
||||
worktree: /tmp/fn_orq_0070_1731612345
|
||||
PR draft: https://github.com/.../pull/123
|
||||
proposals: 3 creadas, 2 auto-aplicadas
|
||||
last run_id: e2e_run_abc123 (status: pass)
|
||||
|
||||
Iter:
|
||||
1. construir → ok (2 funciones nuevas)
|
||||
2. ejecutar → ok
|
||||
3. analizar → fail (2/8 checks)
|
||||
4. mejorar → 3 proposals (2 auto-applicadas)
|
||||
5. construir → ok (re-build tras patches)
|
||||
6. analizar → pass
|
||||
7. recopilador → ok (operations.db integra)
|
||||
|
||||
Siguiente: revisar PR draft + fn proposal list -s pending --target-id 0070
|
||||
```
|
||||
Ver `.claude/commands/autopilot.md` para spec completa.
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
name: autopilot
|
||||
description: Modo full-auto. Pre-flight DoD check, detecta issue vs flow, SIEMPRE delega a fn-orquestador (worktree aislado + PR Gitea). Sin Path inline. Sustituye a /autonomous-task.
|
||||
---
|
||||
|
||||
# /autopilot — Comando autonomo unificado
|
||||
|
||||
Comando UNICO para ejecutar issue o flow autonomo end-to-end. Sustituye a `/autonomous-task` (deprecado). Hace dos cosas:
|
||||
|
||||
1. **Pre-flight DoD readiness check** — sin DoD claro, no arranca.
|
||||
2. **Delega SIEMPRE a `fn-orquestador`** via Agent tool — worktree aislado en `/tmp/fn_orq_<NNNN>_<ts>/`, branch `auto/<NNNN>-<slug>`, PR draft Gitea al converger.
|
||||
|
||||
NO ejecuta nada inline. NO muta cwd del shell del humano. NO duplica worktrees. Toda la complejidad de bucle + paths protegidos + sanity check vive en `fn-orquestador`.
|
||||
|
||||
## Por que solo delegar
|
||||
|
||||
Historico: versiones anteriores de `/autopilot` tenian Path A (delegate a orquestador), Path B (registry-only inline), Path C (flow inline). Los Path B/C reimplementaban lo que ya hace `fn-orquestador` (worktree, branch, PR) y arrastraban un bug: `cd` en Bash de Claude Code PERSISTE entre llamadas → si autopilot hace `cd "$WT"`, todos los Bash subsiguientes operan en branch incorrecta. Solucion: NO hacer Path inline, delegar siempre.
|
||||
|
||||
`fn-orquestador` ahora soporta dos `task_type`:
|
||||
- `issue` — flujo CONSTRUIR→EJECUTAR→RECOPILAR→ANALIZAR→MEJORAR (default).
|
||||
- `flow` — parsea `dev/flows/<NNNN>-*.md` ## Flow y ejecuta steps (Path C absorbido).
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/autopilot <NNNN> # issue NNNN (default si no hay prefijo)
|
||||
/autopilot issue:<NNNN> # issue explicito
|
||||
/autopilot i:<NNNN> # alias
|
||||
/autopilot flow:<NNNN> # flow NNNN
|
||||
/autopilot f:<NNNN> # alias
|
||||
/autopilot check <target> # solo audita DoD readiness, no delega
|
||||
/autopilot <target> --max-iterations N --max-minutes M --dry-run
|
||||
```
|
||||
|
||||
Detector:
|
||||
- `^\d{4}[a-z]?$` → issue (sin prefijo = issue por defecto).
|
||||
- `^(issue|i):\d{4}[a-z]?$` → issue.
|
||||
- `^(flow|f):\d{4}$` → flow.
|
||||
- Otra cosa → ABORT con error de sintaxis.
|
||||
|
||||
## Pre-flight DoD readiness check (OBLIGATORIO)
|
||||
|
||||
Sin DoD claro, autopilot no delega. Verificacion es STOP-gate.
|
||||
|
||||
### Issue (`dev/issues/<NNNN>-*.md`)
|
||||
|
||||
1. Archivo existe en `dev/issues/` (no en `completed/`).
|
||||
2. Frontmatter con `status`, `priority`.
|
||||
3. Al menos UNA de:
|
||||
- `## DoD` o `## Definition of Done` con >=1 bullet/checkbox concreto.
|
||||
- `## Acceptance` con checkboxes `[ ]`.
|
||||
- `## Tests` + `## Tareas` ambas no vacias.
|
||||
4. Tipo declarado/inferible soportado por `fn-orquestador`: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`, `feature_registry_only`.
|
||||
5. NO contiene criterios no-verificables ("queda bonito", "intuitivo", "UX mejor"). Grep simple; si match → ABORT con warning.
|
||||
|
||||
### Flow (`dev/flows/<NNNN>-*.md`)
|
||||
|
||||
1. Archivo existe en `dev/flows/`.
|
||||
2. Frontmatter valido.
|
||||
3. `## Acceptance` con >=1 checkbox.
|
||||
4. `## Flow` no vacio.
|
||||
5. Pre-requisitos declarados.
|
||||
6. Tabla de funciones recomendadas sin `FALTA: crear <id>` (si los hay → ABORT salvo `--allow-construct-missing`).
|
||||
|
||||
Si falla:
|
||||
|
||||
```
|
||||
=== /autopilot check 0125 ===
|
||||
status: NOT READY
|
||||
target: issue 0125 (skill-tree-dashboard-panel)
|
||||
gaps:
|
||||
- Sin seccion DoD/Acceptance
|
||||
- "UX intuitiva" linea 47 — no verificable
|
||||
fix:
|
||||
- Anadir ## DoD con 3-5 bullets programaticamente verificables
|
||||
- Reemplazar criterios subjetivos por mediciones concretas
|
||||
```
|
||||
|
||||
Si OK:
|
||||
|
||||
```
|
||||
=== /autopilot check 0107c ===
|
||||
status: READY
|
||||
target: issue 0107c (refactor data_table)
|
||||
dod_items: 5 checkboxes
|
||||
task_type: refactor_safe
|
||||
estimated_iter: 3-5
|
||||
```
|
||||
|
||||
## Dispatch a fn-orquestador
|
||||
|
||||
Tras pre-flight OK, ejecuta:
|
||||
|
||||
```
|
||||
Agent(
|
||||
subagent_type="fn-orquestador",
|
||||
prompt="""
|
||||
Issue/Flow: <path al .md>
|
||||
Modo: REAL (o --dry-run)
|
||||
task_type: <issue|flow>
|
||||
Pre-condiciones verificadas: 7/7 verde
|
||||
Master: <sha> sync con origin
|
||||
Working tree principal: limpio (baseline)
|
||||
Max iter: N
|
||||
Max min: M
|
||||
Auto-apply proposals: safe
|
||||
Token Gitea: pass gitea/dataforge-git-token
|
||||
DB task_runs: apps/deploy_server/operations.db (schema task_id)
|
||||
Reglas duras: autonomous_loop.md (11 reglas)
|
||||
""",
|
||||
run_in_background=true
|
||||
)
|
||||
```
|
||||
|
||||
Cuando termine, reporta al humano con output canonico del orquestador:
|
||||
|
||||
```
|
||||
=== /autopilot 0121b ===
|
||||
target: issue 0121b (fn doctor e2e-coverage)
|
||||
delegated_to: fn-orquestador
|
||||
status: converged
|
||||
iterations: 1 / 8
|
||||
duration: 4 min / 30
|
||||
task_run_id: task_d285372493cce2e6
|
||||
branch: auto/0121b-orquestador
|
||||
worktree: /tmp/fn_orq_0121b_1779147778
|
||||
PR draft: https://gitea-.../dataforge/fn_registry/pulls/3
|
||||
|
||||
Siguiente: revisar PR, mergear, mover issue a completed/
|
||||
```
|
||||
|
||||
## Reglas duras (autopilot-level)
|
||||
|
||||
1. **Cero cwd mutation**. Autopilot NUNCA hace `cd`. Usa `git -C <repo>` siempre si necesita inspeccionar.
|
||||
2. **Cero ejecucion inline de bucle**. Todo va via `fn-orquestador`. Si autopilot necesita ejecutar algo (pre-flight scripts), es read-only.
|
||||
3. **Cero AskUserQuestion**. Self-pick "Recommended". Si no hay, ABORT con `status=needs_human`.
|
||||
4. **DoD es contrato**. Si DoD no se cumple al final, `task_run.status` queda `partial` y autopilot reporta NOT_DONE — humano decide.
|
||||
5. **Worktree gestion delegada al orquestador**. Autopilot NO crea worktrees propios. NO toca branches.
|
||||
6. **Trazabilidad**: cada decision pre-delegate (especialmente abort de DoD check) se persiste en `task_runs.events_json[]` con `agent: autopilot`.
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Default | Que hace |
|
||||
|---|---|---|
|
||||
| `--max-iterations N` | 10 | Pasado al orquestador |
|
||||
| `--max-minutes M` | 60 | Pasado al orquestador |
|
||||
| `--dry-run` | off | Pasado al orquestador |
|
||||
| `--allow-construct-missing` | off | Flow con `FALTA: crear <id>` → spawn fn-constructor antes |
|
||||
| `--auto-apply-proposals` | `safe` | Pasado al orquestador |
|
||||
|
||||
## Errores canonicos
|
||||
|
||||
| Codigo | Significado | Accion |
|
||||
|---|---|---|
|
||||
| `NOT_READY` | DoD insuficiente | Humano edita .md y relanza |
|
||||
| `needs_human` | Decision ambigua | Humano resuelve y relanza |
|
||||
| `delegated_failed` | fn-orquestador devolvio fail/stall/timeout | Humano lee `task_runs.events_json` |
|
||||
| (resto) | Heredados del orquestador (stalled/timeout/aborted_protected_path/...) | Idem |
|
||||
|
||||
## Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| Hacer Path B/C inline | Mismo bug de cwd mutation que paso 2026-05-19 |
|
||||
| Saltar pre-flight DoD | Trabajar sin contrato = bucle infinito |
|
||||
| Mergear sin tests verde | fn-orquestador ya impide esto, NO bypaseas |
|
||||
| `AskUserQuestion` desde autopilot | Rompe contrato autonomo |
|
||||
| Crear worktree propio en autopilot | Duplica + colision con orquestador (paso 2026-05-19) |
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```bash
|
||||
# Issue con DoD claro
|
||||
/autopilot 0107c
|
||||
|
||||
# Flow con piezas faltantes — autoriza creacion antes
|
||||
/autopilot flow:0008 --allow-construct-missing
|
||||
|
||||
# Solo audit
|
||||
/autopilot check 0125
|
||||
/autopilot check flow:0008
|
||||
|
||||
# Dry run
|
||||
/autopilot 0107c --dry-run
|
||||
```
|
||||
|
||||
## Relacion con otras reglas
|
||||
|
||||
- [[autonomous_loop]] — politica del bucle (sandbox, paths protegidos, watchdog). fn-orquestador la aplica.
|
||||
- [[apps_tbd]] — politica TBD por tipo de cambio.
|
||||
- [[apps_subrepo]] — `git init` dentro de apps nuevas antes de limpiar worktree.
|
||||
- [[feature_flags]] — codigo incompleto detras de flag OFF.
|
||||
- [[registry_calls]] — invocaciones canonicas.
|
||||
- [[e2e_validation]] — `e2e_checks` consumidos por fn-analizador.
|
||||
- [[delegation]] — spawn fn-constructor antes que escribir inline.
|
||||
|
||||
## Migracion desde `/autonomous-task`
|
||||
|
||||
`/autonomous-task` queda DEPRECADO. Sustitucion 1:1:
|
||||
|
||||
| Antes | Ahora |
|
||||
|---|---|
|
||||
| `/autonomous-task 0070` | `/autopilot 0070` |
|
||||
| `/autonomous-task 0070 --max-iterations 15` | `/autopilot 0070 --max-iterations 15` |
|
||||
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
|
||||
|
||||
`/autopilot` anade pre-flight DoD check + detect flow. Behaviour orquestador-side idem.
|
||||
|
||||
## Historico
|
||||
|
||||
- v1 (2026-05-15): introducido con Path A/B/C inline + self-Q&A.
|
||||
- v2 (2026-05-19): simplificado tras incidente cwd mutation en piloto 0121b. Solo delega a fn-orquestador. Self-Q&A movido al orquestador. Sustituye a `/autonomous-task`.
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
description: "Lista todos los slash commands disponibles en el repo: globales de fn_registry + namespaced de cada project. Filtra por substring o por namespace."
|
||||
---
|
||||
|
||||
# /commands — Catalogo de slash commands del repo
|
||||
|
||||
Inventario unificado. Lista los `.md` bajo `.claude/commands/` (recursivo, sigue symlinks) y agrupa por namespace.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/commands # listado completo agrupado por namespace
|
||||
/commands <substring> # filtra por substring en nombre o descripcion
|
||||
/commands --ns <namespace> # solo un namespace (global, aurgi, ...)
|
||||
/commands --json # salida JSON para agentes
|
||||
```
|
||||
|
||||
## Implementacion
|
||||
|
||||
Bash + awk. Parsea frontmatter `description:` de cada `.md`. Agrupa por subdirectorio (subdir = namespace, root = `global`).
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="${FN_REGISTRY_ROOT:-/home/egutierrez/fn_registry}"
|
||||
CMD_DIR="$ROOT/.claude/commands"
|
||||
|
||||
# Recolecta: ns|name|description
|
||||
collect() {
|
||||
find -L "$CMD_DIR" -type f -name '*.md' | while read -r f; do
|
||||
rel="${f#$CMD_DIR/}"
|
||||
case "$rel" in
|
||||
*/*) ns="${rel%%/*}"; name="${rel#*/}"; name="${name%.md}" ;;
|
||||
*) ns="global"; name="${rel%.md}" ;;
|
||||
esac
|
||||
desc=$(awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit}' "$f")
|
||||
printf '%s|%s|%s\n' "$ns" "$name" "${desc:-(sin descripcion)}"
|
||||
done | sort
|
||||
}
|
||||
|
||||
collect | awk -F'|' '
|
||||
{
|
||||
if ($1 != prev_ns) {
|
||||
if (prev_ns) print ""
|
||||
if ($1 == "global") print "## global (/<cmd>)"
|
||||
else print "## " $1 " (/" $1 ":<cmd>)"
|
||||
prev_ns = $1
|
||||
}
|
||||
printf "- /%s%s — %s\n", ($1=="global"?"":$1":"), $2, $3
|
||||
}'
|
||||
```
|
||||
|
||||
Filtros:
|
||||
|
||||
- Substring: `grep -i "<substring>"` sobre stdout.
|
||||
- `--ns X`: filtrar antes del `awk` por `$1 == "X"`.
|
||||
- `--json`: reemplazar el `awk` por `jq -Rsn` que construya array `{namespace, name, description, invocation}`.
|
||||
|
||||
## Salida (formato humano)
|
||||
|
||||
```
|
||||
## global (/<cmd>)
|
||||
- /app — Crear, configurar y desplegar apps del registry
|
||||
- /autopilot — Modo full-auto...
|
||||
- /commands — Catalogo de slash commands del repo
|
||||
...
|
||||
|
||||
## aurgi (/aurgi:<cmd>)
|
||||
- /aurgi:anadir_contexto_aurgi — Anade o modifica contexto...
|
||||
- /aurgi:aumentar_task — Enriquece tarea Aurgi con preguntas...
|
||||
- /aurgi:contexto_aurgi — Aprende el contexto de Aurgi...
|
||||
```
|
||||
|
||||
## Cuando usarlo
|
||||
|
||||
- Sesion nueva: ver de un vistazo que slash commands hay disponibles.
|
||||
- Antes de inventar logica inline: comprobar si ya existe un command.
|
||||
- Auditoria: verificar que los projects exponen sus commands correctamente.
|
||||
- Onboarding: nuevo PC clonado, descubrir capacidades del repo sin abrir N archivos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Sigue symlinks (`find -L`). Si un symlink apunta a directorio inexistente, devuelve vacio para esa rama — verificar con `ls -L .claude/commands/<ns>/`.
|
||||
- Solo escanea `<root>/.claude/commands/`. Commands user-global en `~/.claude/commands/` NO entran (son personales, fuera del repo).
|
||||
- Namespace = nombre del subdirectorio bajo `.claude/commands/`. Coincide con el project pero no por mecanismo — por convencion. Ver `.claude/rules/project_commands.md`.
|
||||
- Para que un command de project aparezca aqui desde la raiz, hace falta el symlink (`.claude/commands/<project>` -> `../../projects/<project>/.claude/commands`).
|
||||
@@ -0,0 +1,274 @@
|
||||
# /cpp-app — Crear o modificar app C++ del registry sin olvidar nada
|
||||
|
||||
Recopila TODOS los datos necesarios (frontmatter, trio app_hub, panels, AppConfig, service block, e2e_checks, uses_functions) **antes** de tocar el disco. Tras confirmar, ejecuta scaffolder o edits, regenera iconos, refresca app_hub, compila y deploya a Windows.
|
||||
|
||||
Sustituye al flujo manual "edito main.cpp + app.md + CMakeLists.txt a mano". Wrapper sobre `init_cpp_app_bash_pipelines` (create) o edits directos sobre `app.md` (modify) + `regenerate_app_icons` + `refresh_app_hub` + `redeploy_cpp_app_windows`.
|
||||
|
||||
---
|
||||
|
||||
## Uso
|
||||
|
||||
```
|
||||
/cpp-app # interactivo, modo create
|
||||
/cpp-app <name> # interactivo, modo create con name pre-rellenado
|
||||
/cpp-app modify <name> # editar app existente
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modo CREATE — flujo turno a turno
|
||||
|
||||
Si `$ARGUMENTS` no empieza por `modify`, es create. Si trae `<name>`, lo usas como default; si no, pregunta name.
|
||||
|
||||
### Paso 0 — verificar que no existe
|
||||
|
||||
```bash
|
||||
test -d "/home/lucas/fn_registry/apps/<name>" \
|
||||
|| ls /home/lucas/fn_registry/projects/*/apps/<name> 2>/dev/null
|
||||
```
|
||||
|
||||
Si existe en cualquier ubicacion: **abortar** y sugerir `/cpp-app modify <name>`. NO sobreescribir.
|
||||
|
||||
### Paso 1 — Identidad (AskUserQuestion)
|
||||
|
||||
1. **name** (texto libre — valida snake_case + contiene verbo segun `ids_naming.md`). Verbos canonicos: `show, render, view, plot, edit, manage, monitor, browse, explore, run, launch, scan, audit, debug, profile, ...`. Si no trae verbo, sugerir alternativas (`viewer` -> `<name>_viewer`).
|
||||
2. **project** (select: ninguno / lista de `projects/*/`). Si ninguno -> `apps/<name>/`.
|
||||
3. **domain** (select: `tools` (default), `gfx`, `tui`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `pipelines`, `browser`).
|
||||
4. **description** 1 linea (texto libre, max 80 chars). **OBLIGATORIO** — sin esto el hub muestra tarjeta vacia.
|
||||
|
||||
### Paso 2 — Trio app_hub OBLIGATORIO
|
||||
|
||||
Regla dura `cpp_apps.md`: description + icon.phosphor + icon.accent SIEMPRE juntos.
|
||||
|
||||
5. **icon.phosphor** glyph name. Antes de preguntar, ofrece busqueda:
|
||||
```bash
|
||||
ls /home/lucas/fn_registry/sources/phosphor-core/assets/fill/ | grep -i "<keyword>"
|
||||
```
|
||||
Sugiere 3-5 candidatos basados en `description`. Default segun domain: `gfx`->`palette`, `tui`->`terminal`, `tools`->`wrench`, `infra`->`gear`, `finance`->`chart-line-up`, `datascience`->`graph`, `cybersecurity`->`shield`.
|
||||
6. **icon.accent** hex `#rrggbb` (palette select):
|
||||
- sky `#0ea5e9`, indigo `#4f46e5`, violet `#7c3aed`, pink `#ec4899`, rose `#f43f5e`, red `#dc2626`, orange `#ea580c`, amber `#d97706`, green `#16a34a`, teal `#0d9488`, cyan `#0891b2`, slate `#475569`. Default segun domain.
|
||||
|
||||
### Paso 3 — Tags
|
||||
|
||||
7. **tags** (multiSelect): `service`, `launcher`, `dashboard`, `viewer`, `editor`, `monitor`, `debug`, `prototype`. Si selecciona `service` -> activar bloque service (Paso 7).
|
||||
|
||||
### Paso 4 — Panels iniciales
|
||||
|
||||
8. **panels** (texto libre o select):
|
||||
- Default: 1 panel `Main` (Ctrl+1).
|
||||
- Opcion lista: hasta 4 paneles. Por cada uno: `{label, shortcut}`. Generara `PanelToggle k_panels[]` en `main.cpp`.
|
||||
|
||||
### Paso 5 — AppConfig flags
|
||||
|
||||
9. (multiSelect):
|
||||
- `init_gl_loader` (true si la app llama `gl*` directo, ej. shaders, GPU renderer custom). Default false.
|
||||
- `viewports` true (default) / false (single-window).
|
||||
- `auto_dockspace` true (default) / false (solo si gestiona DockSpace propio tipo `shaders_lab`).
|
||||
- `fps_overlay` activo de inicio? (controla solo el default; el menu Settings lo toggle).
|
||||
|
||||
### Paso 6 — Funciones del registry a usar
|
||||
|
||||
10. **uses_functions** lista IDs. Antes de preguntar, busca candidatas segun description:
|
||||
```
|
||||
mcp__registry__fn_search query="<keyword>" entity="functions"
|
||||
```
|
||||
Y muestra capability groups relevantes (`docs/capabilities/INDEX.md`). El usuario puede aceptar lista, anadir IDs, o dejar vacio (se rellena tras codear).
|
||||
|
||||
Cada ID que no este en el registry -> ofrecer spawn `fn-constructor` antes de continuar (regla `delegation.md`).
|
||||
|
||||
### Paso 7 — Bloque `service:` (solo si tag=service)
|
||||
|
||||
11. Si paso 3 marco `service`, recopilar (regla `function_tags.md` + issue 0105):
|
||||
- `port` int o null
|
||||
- `health_endpoint` ruta GET o null
|
||||
- `health_timeout_s` (default 3)
|
||||
- `runtime` (select: `systemd-user`, `systemd-system`, `docker-compose`, `stdio`, `manual`)
|
||||
- `systemd_unit` (obligatorio si runtime empieza por `systemd-`)
|
||||
- `systemd_scope` (`user|system|null`)
|
||||
- `restart_policy` (select: `always` (Recommended — gotcha: `on-failure` NO reinicia SIGTERM limpio), `on-failure`, `none`)
|
||||
- `pc_targets` (multiSelect de pc_locations actuales: `aurgi-pc`, `home-wsl`, ...)
|
||||
- `is_local_only` (true/false default false)
|
||||
|
||||
### Paso 8 — Persistencia
|
||||
|
||||
12. (multiSelect):
|
||||
- BD propia SQLite `<name>.db` en `local_files/`? -> recordar usar `fn::local_path("<name>.db")` (cpp_apps.md §7)
|
||||
- operations.db (para entities/relations)? -> ejecutar `fn ops init` tras crear
|
||||
- Archivos config en `local_files/`?
|
||||
|
||||
### Paso 9 — e2e_checks (issue 0068)
|
||||
|
||||
13. Default sugerido (modificable):
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build
|
||||
cmd: "cmake --build cpp/build --target <name> -j"
|
||||
timeout_s: 300
|
||||
- id: self_test
|
||||
cmd: "./cpp/build/apps/<name>/<name> --self-test"
|
||||
timeout_s: 30
|
||||
severity: warning # si todavia no implementa --self-test
|
||||
```
|
||||
Pregunta: ¿anadir mas checks (ops_audit, pytest, smoke)?
|
||||
|
||||
### Paso 10 — Resumen y confirmacion
|
||||
|
||||
Mostrar bloque YAML completo del `app.md` que se va a generar + flags del scaffolder + post-acciones. Pedir confirmacion antes de ejecutar.
|
||||
|
||||
---
|
||||
|
||||
## Modo CREATE — ejecucion
|
||||
|
||||
Una vez confirmado:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
|
||||
# 1. Scaffolder
|
||||
./fn run init_cpp_app <name> \
|
||||
[--project <p>] \
|
||||
[--domain <d>] \
|
||||
--desc "<description>" \
|
||||
[--tags "<csv>"]
|
||||
|
||||
# 2. Editar app.md generado para anadir:
|
||||
# - icon: {phosphor, accent}
|
||||
# - service: {...} (si aplica)
|
||||
# - uses_functions: [...]
|
||||
# - e2e_checks: [...]
|
||||
# (el scaffolder no rellena estos; editarlos con Edit tool)
|
||||
|
||||
# 3. Editar main.cpp generado para reflejar:
|
||||
# - panels[] custom (si != default)
|
||||
# - cfg.init_gl_loader / cfg.auto_dockspace / cfg.viewports
|
||||
# - includes de funciones registry usadas
|
||||
|
||||
# 4. Editar CMakeLists.txt para anadir paths de funciones del registry:
|
||||
# ${CMAKE_SOURCE_DIR}/functions/<d>/<f>.cpp
|
||||
|
||||
# 5. Si es service -> ofrecer crear systemd unit (skipear si runtime=stdio|manual)
|
||||
|
||||
# 6. Si pidio operations.db
|
||||
./fn ops init apps/<name> # o projects/<p>/apps/<name>
|
||||
|
||||
# 7. Generar icono
|
||||
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
|
||||
|
||||
# 8. Indexar
|
||||
./fn index
|
||||
|
||||
# 9. Compilar Windows
|
||||
./fn run redeploy_cpp_app_windows <name> <dir> --build
|
||||
|
||||
# 10. Refrescar app_hub
|
||||
./fn run refresh_app_hub
|
||||
|
||||
# 11. Auditoria
|
||||
./fn doctor cpp-apps
|
||||
[[ "<tag>" == *service* ]] && ./fn doctor services-spec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modo MODIFY — flujo
|
||||
|
||||
`/cpp-app modify <name>`
|
||||
|
||||
### Paso 0 — Localizar
|
||||
|
||||
```bash
|
||||
# Buscar apps/<name>/ o projects/*/apps/<name>/
|
||||
sqlite3 /home/lucas/fn_registry/registry.db \
|
||||
"SELECT id, dir_path FROM apps WHERE name='<name>' AND lang='cpp';"
|
||||
```
|
||||
|
||||
Si no existe: abortar, sugerir `/cpp-app` (sin args) para crear.
|
||||
|
||||
### Paso 1 — Mostrar config actual
|
||||
|
||||
```bash
|
||||
mcp__registry__fn_show id="<id>"
|
||||
cat <dir>/app.md
|
||||
```
|
||||
|
||||
### Paso 2 — Que cambiar (multiSelect)
|
||||
|
||||
- `description` (1 linea)
|
||||
- `icon.phosphor` o `icon.accent`
|
||||
- `tags` (anadir/quitar; si toca `service` -> Paso 7 del create)
|
||||
- `uses_functions` (anadir/quitar — recordar editar CMakeLists.txt)
|
||||
- `panels` (anadir/quitar/renombrar)
|
||||
- `service:` block (si tag=service)
|
||||
- `e2e_checks`
|
||||
- `domain`
|
||||
- `rename` (cambia name, dir, IDs derivados, repo Gitea — operacion delicada, requiere doble confirmacion)
|
||||
|
||||
### Paso 3 — Aplicar cambios
|
||||
|
||||
Para cada cambio: usa `Edit` sobre los archivos correspondientes. NUNCA `Write` completo de `app.md` (preserva campos que no toques).
|
||||
|
||||
### Paso 4 — Post-acciones (segun lo que toco)
|
||||
|
||||
```bash
|
||||
# Siempre
|
||||
cd /home/lucas/fn_registry && ./fn index
|
||||
|
||||
# Si toco icon.* -> regenerar appicon
|
||||
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
|
||||
|
||||
# Si toco trio o panels o uses_functions o cambia code:
|
||||
./fn run redeploy_cpp_app_windows <name> <dir> --build
|
||||
|
||||
# Si toco description o icon o tags:
|
||||
./fn run refresh_app_hub
|
||||
|
||||
# Si toco service: o tag service
|
||||
./fn doctor services-spec
|
||||
|
||||
# Siempre al final
|
||||
./fn doctor cpp-apps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas duras
|
||||
|
||||
- **NUNCA** crear `main.cpp` + `CMakeLists.txt` + `app.md` a mano. Siempre via `init_cpp_app_bash_pipelines` (regla `cpp_apps.md`).
|
||||
- **NUNCA** poner el codigo en `cpp/apps/<n>/`. Solo `apps/<n>/` o `projects/<p>/apps/<n>/`.
|
||||
- **NUNCA** dejar `app.md` sin el trio (description + icon.phosphor + icon.accent). Tarjeta del hub queda gris.
|
||||
- **NUNCA** declarar funciones del registry en `uses_functions` sin listar su `.cpp` en `CMakeLists.txt` (drift detectado por `fn doctor uses-functions`).
|
||||
- **NUNCA** usar `Restart=on-failure` en systemd unit de un service C++ — gotcha 2026-05-17 (`sqlite_api.service` cayo 20h). Default `Restart=always`.
|
||||
- Despues de **cualquier** cambio en el trio: `regenerate_app_icons <name>` + `refresh_app_hub`.
|
||||
|
||||
---
|
||||
|
||||
## Auto-verificacion final
|
||||
|
||||
Tras crear o modificar, reportar al usuario:
|
||||
|
||||
```
|
||||
=== app <name> ===
|
||||
dir: <abs_dir>
|
||||
domain: <d>
|
||||
description: "<desc>"
|
||||
icon: <phosphor> + <accent>
|
||||
tags: [<csv>]
|
||||
uses_functions: N funciones (<list_top_5>)
|
||||
panels: N (<labels>)
|
||||
e2e_checks: N checks
|
||||
service: <si/no — port:<p> health:<h>>
|
||||
|
||||
Acciones ejecutadas:
|
||||
[✓] scaffolder / edits
|
||||
[✓] generate_app_icon
|
||||
[✓] fn index (registry.db actualizado)
|
||||
[✓] redeploy_cpp_app_windows (Desktop/apps/<name>/<name>.exe)
|
||||
[✓] refresh_app_hub (tarjeta visible en hub)
|
||||
[✓] fn doctor cpp-apps (limpio | N warnings)
|
||||
|
||||
Siguiente paso sugerido:
|
||||
- Abrir app_hub_launcher en Windows y verificar tarjeta
|
||||
- Anadir tests visuales si la app tiene paneles propios (cpp/PATTERNS.md §11)
|
||||
```
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: fix-issue
|
||||
description: Implementar un issue de dev/issues/ end-to-end. Crea rama, ejecuta tareas, bumpa version si toca modulos/framework/apps (via /version), tests, cierra issue, integra a master.
|
||||
---
|
||||
|
||||
# /fix-issue
|
||||
|
||||
Ejecuta el flujo completo de implementacion/cierre de un issue de `dev/issues/`. Adaptado al stack del registry: Go (`-tags fts5 CGO_ENABLED=1`), Python (`python/.venv/bin/python3`), Bash, TypeScript (`pnpm`), C++ (`cmake`+`mingw-w64` toolchain).
|
||||
|
||||
## Inputs
|
||||
|
||||
```
|
||||
/fix-issue <NNNN[a|b|c...]>
|
||||
```
|
||||
|
||||
- `NNNN`: numero del issue (ej. `0107`).
|
||||
- Si es sub-issue, sufijo letra: `0107a`, `0107b`, ...
|
||||
|
||||
Si no se proporciona, preguntar.
|
||||
|
||||
## Flujo obligatorio
|
||||
|
||||
### 1. Resolver el issue
|
||||
|
||||
- `dev/issues/<NNNN>-*.md` → si no existe, STOP.
|
||||
- Si ya en `dev/issues/completed/`, STOP.
|
||||
- Si es sub-issue, leer tambien el principal para contexto.
|
||||
|
||||
### 2. Leer y extraer
|
||||
|
||||
- Objetivo, tareas, arquitectura, prerequisitos, riesgos.
|
||||
- Identificar archivos afectados — anotar si toca:
|
||||
- `modules/<X>/` o `cpp/framework/` → bumpa version (paso 8).
|
||||
- `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/` → indexer + `fn index` al cerrar.
|
||||
- Apps en `apps/<X>/` o `projects/*/apps/<X>/` → requiere rama TBD (regla `apps_tbd.md`) **+ bumpa version per-app (paso 8)**. Si el issue toca multiples apps, una llamada `/version` por app.
|
||||
- Registry meta (CLAUDE.md, rules, templates) → push directo a master OK.
|
||||
|
||||
### 3. Estrategia de rama
|
||||
|
||||
**Registry-only changes** (functions/types/docs/rules):
|
||||
- Push directo a master OK. NO crear rama.
|
||||
|
||||
**Apps changes** (apps/, projects/*/apps/):
|
||||
- Crear rama TBD:
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
git checkout -b issue/<NNNN>-<slug>
|
||||
```
|
||||
La rama es del registry. Si la app es sub-repo, ademas crear rama dentro del sub-repo.
|
||||
|
||||
**Modules/framework changes** (`modules/`, `cpp/framework/`):
|
||||
- Rama TBD obligatoria (afecta a todas las apps que linkean).
|
||||
|
||||
### 4. Plan con TaskCreate
|
||||
|
||||
- Crear tarea por bloque logico del issue.
|
||||
- Incluir SIEMPRE:
|
||||
- Tarea de tests (unit + smoke).
|
||||
- Tarea de `fn index` si toco metadata.
|
||||
- Tarea de `/version` si toco `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/` (una llamada por target).
|
||||
- Tarea de cleanup/docs.
|
||||
|
||||
### 5. Implementar
|
||||
|
||||
Reglas registry-first (CLAUDE.md):
|
||||
- ANTES de escribir codigo reutilizable → `mcp__registry__fn_search` para encontrar lo que existe.
|
||||
- Si falta funcion reutilizable → spawn `fn-constructor` (no escribir inline).
|
||||
- Si patron se repite >2x → propose nueva funcion.
|
||||
- NUNCA `sqlite3 registry.db "SELECT ..."` plano — usar MCP.
|
||||
|
||||
Convenciones del stack:
|
||||
|
||||
| Stack | Build/test |
|
||||
|---|---|
|
||||
| Go | `CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/` y `CGO_ENABLED=1 go test -tags fts5 ./...` |
|
||||
| Python | `python/.venv/bin/python3 -m pytest <path>` |
|
||||
| Bash | `bash -n <script>.sh` + tests inline |
|
||||
| TypeScript | `cd frontend && pnpm build && pnpm test` |
|
||||
| C++ (Linux) | `cmake --build build --target <app>` |
|
||||
| C++ (Windows MinGW) | `cmake -B build/windows -DCMAKE_TOOLCHAIN_FILE=cpp/toolchains/mingw-w64.cmake && cmake --build build/windows --target <app>` |
|
||||
|
||||
Commits atomicos por bloque logico con prefijos: `feat:`, `fix:`, `test:`, `docs:`, `refactor:`, `chore:`. Mensajes en espanol. NO WIP.
|
||||
|
||||
### 6. Tests
|
||||
|
||||
Stack-dependent (ver arriba). Si tests pasan parcialmente con failures pre-existentes no causadas por la rama, documentar en cuerpo del commit/PR.
|
||||
|
||||
### 7. Feature flags (si aplica)
|
||||
|
||||
Si el issue forma parte de un feature multi-issue:
|
||||
- Editar `dev/feature_flags.json` con el flag (desactivado).
|
||||
- Activar el flag en el ultimo sub-issue del set.
|
||||
|
||||
Flag != WIP. Codigo detras de flag debe compilar + testear.
|
||||
|
||||
### 8. Version bump (si toco modulos/framework/apps)
|
||||
|
||||
**OBLIGATORIO si el issue toco** alguno de:
|
||||
- `modules/<X>/` → bumpa `modules/<X>/module.md::version`.
|
||||
- `cpp/framework/` → bumpa `modules/framework/module.md::version`.
|
||||
- `apps/<X>/` → bumpa `apps/<X>/app.md::version`.
|
||||
- `projects/<P>/apps/<X>/` → bumpa `projects/<P>/apps/<X>/app.md::version`.
|
||||
|
||||
```
|
||||
/version <path> <major|minor|patch> "<reason>"
|
||||
```
|
||||
|
||||
Reglas (modulos/framework):
|
||||
- Major: breaking ABI/API publica.
|
||||
- Minor: additive (nuevo helper, refactor interno sin cambio de API, nuevo miembro).
|
||||
- Patch: bugfix puro.
|
||||
|
||||
Reglas (apps):
|
||||
- Major: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||
- Minor: feature aditiva visible (nuevo panel, endpoint, opcion).
|
||||
- Patch: bugfix sin cambio observable, refactor interno, mejora perf.
|
||||
|
||||
**Una llamada `/version` por target afectado**. Si el issue toca 1 modulo + 2 apps -> 3 llamadas a `/version` (cada una con su `reason` y bump-type apropiado; pueden diferir).
|
||||
|
||||
Diff guard: cambios que solo tocan el `app.md` (correccion typo descripcion, anadir tag) NO requieren bump — son metadata, no comportamiento. Detectar con `git diff --name-only | grep -v '\.md$'` para decidir si hay cambio de codigo real.
|
||||
|
||||
`/version` solo edita + stage. NO commit. El bump va junto con el codigo correspondiente en el mismo commit (`feat:` o `fix:` o `refactor:`).
|
||||
|
||||
Si NO toco modulos/framework/apps, saltar este paso.
|
||||
|
||||
### 9. Cerrar el issue
|
||||
|
||||
Mover archivo:
|
||||
```bash
|
||||
mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/
|
||||
```
|
||||
|
||||
Actualizar `dev/issues/README.md`:
|
||||
- Link → `completed/<NNNN>-<slug>.md`
|
||||
- Estado → `completado`
|
||||
|
||||
Si es feature multi-issue y este es el ultimo sub-issue:
|
||||
- Flip flag en `dev/feature_flags.json` a `enabled: true` con `enabled_at: <YYYY-MM-DD>`.
|
||||
- Verificar que todos los sub-issues estan en `completed/`.
|
||||
|
||||
### 10. Integrar
|
||||
|
||||
**Registry-only changes**: push directo a master.
|
||||
|
||||
**Apps/modules/framework changes**: `/full-git-push` o `/git-push` (merge --no-ff de la rama a master, push, delete rama).
|
||||
|
||||
### 11. Verificar post-cierre
|
||||
|
||||
- `fn index` — registry.db al dia.
|
||||
- `fn doctor` (subcomandos relevantes: `artefacts`, `services`, `cpp-apps`, `uses-functions`).
|
||||
- Si toco modulos: `fn doctor modules` (post 0107a) — 0 drift.
|
||||
|
||||
## Reglas criticas
|
||||
|
||||
- **Registry-first**: SIEMPRE buscar antes de escribir; delegar a `fn-constructor` antes que inline.
|
||||
- **TBD para apps**: NUNCA push directo a master en apps. Rama corta, merge --no-ff.
|
||||
- **TBD NO para registry**: push directo OK para functions/types/docs/rules.
|
||||
- **`/version` obligatorio** si tocas modulos, framework o apps (con cambio de codigo real, no solo metadata). Si no, drift entre `version:` y `## Capability growth log` y se pierde trazabilidad.
|
||||
- **Tests siempre**: no cerrar issue sin tests pasando (salvo failures pre-existentes documentados).
|
||||
- **Commits atomicos**: 1 commit = 1 bloque logico. No mezclar `feat:` + `test:` en mismo commit.
|
||||
- **Cerrar siempre**: nunca dejar issue implementado sin mover a `completed/` + actualizar README.
|
||||
|
||||
## Referenciado desde
|
||||
|
||||
- `.claude/commands/version.md` — bump semver de modulos.
|
||||
- `.claude/commands/full-git-push.md` — push del registry + sub-repos.
|
||||
- `.claude/rules/apps_tbd.md` — politica de TBD por tipo de cambio.
|
||||
|
||||
## Ejemplo: implementar 0107c (refactor data_table)
|
||||
|
||||
```
|
||||
/fix-issue 0107c
|
||||
|
||||
1. Resolver: dev/issues/0107c-split-data-table.md ✓
|
||||
2. Extraer: refactor 4777 LOC → 6 sub-funciones. Toca modules/ → /version obligatorio.
|
||||
3. Rama: issue/0107c-split-data-table desde master.
|
||||
4. Plan: 8 tareas (lectura + 6 sub-funciones + entrypoint thin + version bump).
|
||||
5. Implementar: spawn fn-constructor en paralelo si hay >1 sub-funcion independiente.
|
||||
6. Tests: build + smoke + primitives_gallery --capture diff.
|
||||
7. Flag: parte de modules-v2, NO activar todavia (espera 0107a-f cerrar).
|
||||
8. /version modules/data_table major "split data_table.cpp into 6 sub-functions"
|
||||
9. Cerrar: mv → completed/ + README.
|
||||
10. /git-push.
|
||||
11. fn index + fn doctor modules → 0 drift en consumidores limpiados.
|
||||
```
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
description: "Gestiona flows (casos de uso multi-app reutilizables) en dev/flows/. Subcomandos: create, list, show, status, done. Runner automatizado en fase 2."
|
||||
---
|
||||
|
||||
# /flow — Gestionar flows del registry
|
||||
|
||||
Flows = casos de uso end-to-end que prueban / ejercitan el sistema multi-app. Viven en `dev/flows/NNNN-<slug>.md`. Cada flow describe Goal + Flow steps + Acceptance checkboxes + Telemetria.
|
||||
|
||||
**OBLIGATORIO antes de `create`**: lee `dev/flows/AGENT_GUIDE.md`. Define donde buscar piezas (capability groups, FTS por tag, apps existentes, vaults), reglas duras para no inventar IDs, y plantilla de razonamiento para recomendar extractor / transformer / sink / scheduler / notify por flow.
|
||||
|
||||
Cada flow nuevo cita IDs reales del registry. Si una pieza falta, escribir `FALTA: crear <id>` en la tabla correspondiente. Nada de inventar nombres.
|
||||
|
||||
Diferencia con `dev/issues/`:
|
||||
- Issues = bugs / features de implementacion.
|
||||
- Flows = trabajos reutilizables que cruzan varias apps.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/flow create <slug> # nuevo flow desde template, ID auto
|
||||
/flow list # tabla resumen
|
||||
/flow show <NNNN> # imprime contenido + acceptance %
|
||||
/flow status <NNNN> # status + acceptance % + ultima run
|
||||
/flow done <NNNN> [--notes "..."] # cierra flow (status=done, mueve a completed/)
|
||||
/flow run <NNNN> # fase 2 — runner automatizado (NO IMPLEMENTADO)
|
||||
```
|
||||
|
||||
## Implementacion por subcomando
|
||||
|
||||
### `create <slug>`
|
||||
|
||||
Pasos:
|
||||
1. Valida `<slug>` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error.
|
||||
2. Comprueba que no existe ya: `ls dev/flows/*-<slug>.md`. Si existe, error.
|
||||
3. Calcula siguiente ID libre:
|
||||
- `ls dev/flows/*.md dev/flows/completed/*.md | grep -oE '^dev/flows/(completed/)?[0-9]{4}' | sort -u | tail -1`
|
||||
- Suma 1, zero-pad a 4 digitos.
|
||||
4. Lee `dev/flows/template.md`.
|
||||
5. Sustituye `<slug>`, `NNNN`, `YYYY-MM-DD` (hoy).
|
||||
6. Escribe `dev/flows/NNNN-<slug>.md`.
|
||||
7. Append fila a `dev/flows/INDEX.md` (mantener orden por ID asc).
|
||||
8. Reporta path nuevo + recordatorio "edita Goal / Flow / Acceptance".
|
||||
|
||||
### `list`
|
||||
|
||||
Lee `dev/flows/INDEX.md` y lo imprime tal cual. Si flag `--pending` solo pending, `--done` solo done, `--app <name>` filtra por app.
|
||||
|
||||
Tambien anade columna `Accept%` calculada desde body:
|
||||
- Para cada flow .md, cuenta `[ ]` y `[x]` en seccion `## Acceptance`.
|
||||
- `% = checked / total * 100` redondeo entero.
|
||||
|
||||
### `show <NNNN>`
|
||||
|
||||
`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error.
|
||||
|
||||
### `status <NNNN>`
|
||||
|
||||
Imprime resumen del frontmatter + acceptance %:
|
||||
|
||||
```
|
||||
=== flow 0001 ===
|
||||
name: hn-top-stories
|
||||
status: pending
|
||||
risk: low
|
||||
priority: high
|
||||
apps: navegator_dashboard, dag_engine, data_factory, agents_and_robots
|
||||
acceptance: 2/6 (33%)
|
||||
updated: 2026-05-16
|
||||
|
||||
Pending checks:
|
||||
- [ ] Recipe creada y validada
|
||||
- [ ] DAG corre OK 2 veces consecutivas via scheduler
|
||||
- [ ] data_factory.runs tiene >=2 entries
|
||||
- [ ] Schema extraido cubre 6/6 fields
|
||||
```
|
||||
|
||||
### `done <NNNN> [--notes "..."]`
|
||||
|
||||
Pasos:
|
||||
1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente".
|
||||
2. Edita frontmatter: `status: done`, `updated: <hoy>`.
|
||||
3. Si `--notes`, append a seccion `## Notas`.
|
||||
4. `git mv dev/flows/NNNN-<slug>.md dev/flows/completed/`.
|
||||
5. Actualiza `dev/flows/INDEX.md`: cambia status del flow + mueve fila a seccion Completed (mantener tabla principal solo con pending/running/failed/deferred).
|
||||
|
||||
### `run <NNNN>` — FASE 2 (NO IMPLEMENTADO AUN)
|
||||
|
||||
Hoy: imprime `/flow run no implementado todavia. Sigue los pasos manualmente y marca acceptance con sed/edit.`
|
||||
|
||||
Diseño futuro:
|
||||
- Parsea `## Flow` en pasos.
|
||||
- Cada paso tipo `function: <id>` -> ejecuta `./fn run <id>`.
|
||||
- Cada paso tipo `cmd: <bash>` -> ejecuta subprocess.
|
||||
- Texto libre -> "MANUAL: <text>" + pause user input.
|
||||
- Persistencia ejecuciones en `dev/flows/runs/<id>-<timestamp>.jsonl`.
|
||||
- Update acceptance checkboxes automaticamente segun heuristics (count runs en data_factory, etc.).
|
||||
|
||||
## Conventions
|
||||
|
||||
- Numeracion 0001+, propia (no comparte con `dev/issues/`).
|
||||
- Status: `pending | running | done | failed | deferred`.
|
||||
- Risk: `low` (publico) | `medium` (auth no sensible) | `high` (datos personales).
|
||||
- Apps listadas en frontmatter — `/flow list --app navegator_dashboard` filtra.
|
||||
- Acceptance es la fuente de verdad del progreso.
|
||||
|
||||
## Output style
|
||||
|
||||
Caveman. Tablas markdown. Sin emojis. Sin verbosidad.
|
||||
|
||||
Errores: 1 linea con el problema + sugerencia.
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```
|
||||
/flow create reddit-sentiment-tracker
|
||||
# crea dev/flows/0008-reddit-sentiment-tracker.md
|
||||
# anade fila a INDEX
|
||||
|
||||
/flow list --pending
|
||||
# muestra solo flows no cerrados
|
||||
|
||||
/flow status 0001
|
||||
# acceptance 0/6, todos los checks pendientes
|
||||
|
||||
# Tras correr el flow manualmente:
|
||||
# editas el .md, marcas [x] los checks completados
|
||||
/flow status 0001
|
||||
# acceptance 6/6
|
||||
/flow done 0001 --notes "smoke pass; LLM tardo 14s; recipe robusta"
|
||||
# mueve a completed/, marca status=done
|
||||
```
|
||||
@@ -152,6 +152,20 @@ Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo:
|
||||
cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose
|
||||
```
|
||||
|
||||
### 5b. MEMORIZE — anadir cada funcion nueva a MEMORY.md (issue 0087 pieza 6)
|
||||
|
||||
Por cada funcion creada con exito, llama:
|
||||
|
||||
```bash
|
||||
bash "$ROOT/.claude/scripts/append_fn_to_memory.sh" "<fn_id>" "<one-line purpose>"
|
||||
```
|
||||
|
||||
El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_<id>.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-<id>](reference_fn_<id>.md) — <purpose>`. Asi proximas sesiones cargan MEMORY.md y ven el catalogo de funciones recien creadas sin segunda lookup.
|
||||
|
||||
`purpose` = 1 frase derivada del `description` del .md de la funcion (max 80 chars). Si description es larga, recorta. Ejemplo:
|
||||
- fn_id: `parse_http_log_go_infra`
|
||||
- purpose: "parsea log Apache/Nginx a struct; pure"
|
||||
|
||||
Reporta:
|
||||
- N funciones nuevas creadas (con IDs)
|
||||
- N proposals nuevas en `registry.db.proposals`
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
description: "Gestiona issues del registry en dev/issues/. Subcomandos: list, show, status, board, dep, roadmap, tag, done, stale, create. Frontmatter YAML canonico (issue 0100)."
|
||||
---
|
||||
|
||||
# /issue — Gestionar issues del registry
|
||||
|
||||
Issues viven en `dev/issues/NNNN-<slug>.md` con frontmatter YAML canonico (id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags).
|
||||
|
||||
Allowlists en `dev/TAXONOMY.md` (no inventar valores).
|
||||
|
||||
Diferencia con `dev/flows/`:
|
||||
- **Issues** = bugs, features, refactors, chores, epics de implementacion.
|
||||
- **Flows** = casos de uso end-to-end multi-app.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/issue list [--domain X] [--type Y] [--status Z] [--prio P] [--epic NNNN]
|
||||
/issue show NNNN
|
||||
/issue status NNNN # acceptance % + estado deps
|
||||
/issue board # kanban pendiente/in-progress/bloqueado/done
|
||||
/issue dep NNNN # arbol bloquea/depende
|
||||
/issue roadmap NNNN # epic + sub-IDs (NNNNa, NNNNb, ...)
|
||||
/issue tag NNNN +X -Y # mantenimiento tags/domain
|
||||
/issue done NNNN # mueve a completed/, valida deps
|
||||
/issue stale [--days 30]
|
||||
/issue create <slug> --type T --domain D [--prio P] [--depends NNNN]
|
||||
```
|
||||
|
||||
## Implementacion
|
||||
|
||||
**Fase 1 (manual via Claude):**
|
||||
|
||||
El agente lee `dev/issues/*.md`, parsea frontmatter YAML con `yaml.safe_load`, aplica el filtro, imprime tabla.
|
||||
|
||||
```python
|
||||
import yaml, pathlib, re
|
||||
issues = []
|
||||
for f in pathlib.Path("dev/issues").glob("*.md"):
|
||||
if f.name in {"README.md", "template.md"}: continue
|
||||
txt = f.read_text()
|
||||
m = re.match(r"^---\n(.*?)\n---", txt, re.S)
|
||||
if not m: continue
|
||||
fm = yaml.safe_load(m.group(1)) or {}
|
||||
fm["_path"] = str(f)
|
||||
issues.append(fm)
|
||||
# filter + print
|
||||
```
|
||||
|
||||
**Fase 2 (cuando 0101 dev_console exista):**
|
||||
|
||||
Cada subcomando se mapea a `./apps/dev_console/dev_console issue <subcomando> $ARGS`.
|
||||
|
||||
## Subcomandos clave
|
||||
|
||||
### `list`
|
||||
|
||||
Imprime tabla `id | title | status | type | domain | priority | depends_pending`. Filtrable por flags.
|
||||
|
||||
### `show NNNN`
|
||||
|
||||
Read directo del .md + render del frontmatter como tabla + body como markdown.
|
||||
|
||||
### `status NNNN`
|
||||
|
||||
Cuenta checkboxes en `## Acceptance` + chequea si todos los `depends` estan en `status: completado`. Si alguno no, marca `bloqueado`.
|
||||
|
||||
### `board`
|
||||
|
||||
Tabla 4 columnas (pendiente / in-progress / bloqueado / completado_hoy). Card por issue: id + title + prio. Status `bloqueado` se calcula on-the-fly desde `depends`.
|
||||
|
||||
### `roadmap NNNN`
|
||||
|
||||
Si `type: epic`: lista sub-issues `NNNNa`, `NNNNb`, etc. con su estado. Si no epic: error "not an epic".
|
||||
|
||||
### `done NNNN`
|
||||
|
||||
1. Lee frontmatter.
|
||||
2. Verifica todos `depends` cerrados (sino, error).
|
||||
3. Cuenta `## Acceptance` 100% (sino, error).
|
||||
4. `git mv dev/issues/NNNN-*.md dev/issues/completed/`.
|
||||
5. Actualiza `status: completado` + `updated: today`.
|
||||
|
||||
### `create <slug> --type T --domain D`
|
||||
|
||||
Genera siguiente ID libre (max existing + 1, zero-padded 4). Scaffold desde plantilla minima con frontmatter rellenado.
|
||||
|
||||
## Reglas
|
||||
|
||||
- Domain debe estar en `dev/TAXONOMY.md` allowlist.
|
||||
- Scope/type/priority idem.
|
||||
- `id` siempre string `"NNNN"` (zero-padded, sub-IDs con sufijo `a-z`).
|
||||
- Modificar frontmatter SIEMPRE preserva campos no tocados (no overwrite).
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: version
|
||||
description: Bumpear semver de un modulo, framework, paquete o app del registry. Edita <target>.md::version + ## Capability growth log. NO commitea.
|
||||
---
|
||||
|
||||
# /version
|
||||
|
||||
Bumpea la version de un **modulo, framework, paquete o app** del registry siguiendo SemVer estricto y mantiene el `## Capability growth log` sincronizado con `<target>.md::version`.
|
||||
|
||||
Disenado para usarse desde `/fix-issue` cuando el cambio afecte:
|
||||
- `modules/<X>/` (cualquier modulo C++) — edita `module.md`
|
||||
- `cpp/framework/` — edita `modules/framework/module.md`
|
||||
- `apps/<X>/` o `projects/<P>/apps/<X>/` — edita `app.md`
|
||||
- Otros paquetes versionados con `<target>.md` y campo `version:`
|
||||
|
||||
## Inputs
|
||||
|
||||
```
|
||||
/version <path> <major|minor|patch> "<reason>"
|
||||
```
|
||||
|
||||
- `<path>`: directorio del target (ej. `modules/data_table`, `cpp/framework`, `apps/chart_demo`, `projects/fn_monitoring/apps/registry_dashboard`).
|
||||
- `<major|minor|patch>`: tipo de bump SemVer.
|
||||
- `<reason>`: 1-frase humana — lo que cambia. Se inserta en el log.
|
||||
|
||||
## Resolucion del archivo target
|
||||
|
||||
| Path empieza por | Archivo a editar |
|
||||
|---|---|
|
||||
| `modules/` | `<path>/module.md` |
|
||||
| `cpp/framework` | `modules/framework/module.md` |
|
||||
| `apps/` | `<path>/app.md` |
|
||||
| `projects/*/apps/` | `<path>/app.md` |
|
||||
| `projects/*/analysis/` | `<path>/analysis.md` |
|
||||
|
||||
Si no encuentra archivo target -> ERROR.
|
||||
|
||||
## Reglas SemVer
|
||||
|
||||
### Modulos / framework
|
||||
|
||||
| Bump | Cuando |
|
||||
|---|---|
|
||||
| `major` | Cambios breaking en API publica: firma de entry function, layout de State struct expuesto, eliminacion de members, cambio incompatible de comportamiento. |
|
||||
| `minor` | Adiciones backwards-compatible: nuevo evento opt-in, nuevo renderer, nuevo helper, nuevo miembro. |
|
||||
| `patch` | Bugfix sin cambio de API. |
|
||||
|
||||
Refactor interno SIN cambio de API publica -> `minor` (no major).
|
||||
|
||||
### Apps
|
||||
|
||||
| Bump | Cuando |
|
||||
|---|---|
|
||||
| `major` | Breaking observable por usuarios: CLI args incompatibles, schema BBDD propia rompe lectores viejos, formato wire (HTTP/gRPC) incompatible, eliminacion de panel/feature que la gente usaba. |
|
||||
| `minor` | Feature aditiva: nuevo panel, nuevo endpoint, nueva opcion CLI, nueva tab, mejora visible no rompedora. |
|
||||
| `patch` | Bugfix sin cambio observable. Refactor interno. Mejoras de perf. |
|
||||
|
||||
Bump de **dependencia** (modulo/funcion del registry) que mejora la app pero la app no cambia su API -> `patch` (la app no es responsable de la mejora; el modulo si).
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Validar input
|
||||
|
||||
- `<target_file>` existe -> si no, ERROR.
|
||||
- Bump type en {major, minor, patch} -> si no, ERROR.
|
||||
- Reason no vacia -> si no, ERROR.
|
||||
|
||||
### 2. Leer version actual
|
||||
|
||||
Parsear frontmatter. Buscar `version: X.Y.Z`. Si no existe:
|
||||
- Para `module.md` -> ERROR "module.md sin campo version".
|
||||
- Para `app.md` -> asumir `0.1.0` (baseline) e insertar el campo despues de `domain:`.
|
||||
|
||||
### 3. Calcular proxima version
|
||||
|
||||
```
|
||||
1.4.0 + major = 2.0.0
|
||||
1.4.0 + minor = 1.5.0
|
||||
1.4.0 + patch = 1.4.1
|
||||
```
|
||||
|
||||
Major bump -> minor y patch a 0. Minor bump -> patch a 0.
|
||||
|
||||
### 4. Editar `<target_file>`
|
||||
|
||||
Cambiar linea `version: <old>` por `version: <new>`.
|
||||
|
||||
### 5. Anadir entrada a `## Capability growth log`
|
||||
|
||||
Insertar al inicio de la lista (lineas posteriores al header `## Capability growth log`):
|
||||
|
||||
```markdown
|
||||
- v<new> (<fecha YYYY-MM-DD>) — <reason>
|
||||
```
|
||||
|
||||
Si la seccion no existe -> crearla al final del archivo antes de `## Notes` (o al final si no hay Notes).
|
||||
|
||||
### 6. Verificar drift de members (solo modulos, opcional)
|
||||
|
||||
Si la herramienta `fn doctor modules` existe (post 0107a) y el target es modulo:
|
||||
- Compara `members:` actual vs ultima version registrada en `registry.db::modules_history`.
|
||||
- Si hay diff en members y bump es `patch` -> WARNING.
|
||||
- Si hay diff en API publica y bump no es `major` -> ERROR (require `--force`).
|
||||
|
||||
No aplica a apps (no tienen `members:`).
|
||||
|
||||
### 7. Stage en git
|
||||
|
||||
`git add <target_file>`. NO commit. El commit final lo hace el flujo padre.
|
||||
|
||||
### 8. Reportar
|
||||
|
||||
```
|
||||
/version apps/chart_demo minor "anade tab radar chart"
|
||||
|
||||
apps/chart_demo/app.md
|
||||
version: 1.2.0 -> 1.3.0
|
||||
## Capability growth log: + v1.3.0 (2026-05-18) — anade tab radar chart
|
||||
|
||||
Staged. NO committed.
|
||||
Next: terminar el fix-issue y hacer commit con el resto de cambios.
|
||||
```
|
||||
|
||||
## Reglas criticas
|
||||
|
||||
- **NUNCA commit**. `/version` solo edita + stage. El commit lo hace el flujo padre (`/fix-issue`, `/git-push`).
|
||||
- **NUNCA saltar version**. No 1.4.0 -> 1.4.2 directo.
|
||||
- **NUNCA bajar version**. Si rollback, crea nueva version superior con comportamiento viejo restaurado.
|
||||
- **fecha = HOY** (`date +%Y-%m-%d`).
|
||||
- **reason** comprensible sin contexto del PR actual.
|
||||
|
||||
## Referenciado desde
|
||||
|
||||
- `/fix-issue` — al detectar cambios en `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/`, sugiere ejecutar `/version` antes del commit final.
|
||||
- `.claude/rules/cpp_apps.md` — politica de bump.
|
||||
- `dev/issues/0107-modules-standardization.md` — origen del flujo (modulos).
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```
|
||||
# Bug fix en data_table (modulo)
|
||||
/version modules/data_table patch "fix off-by-one en seleccion multi-row con shift+click"
|
||||
# -> 1.4.0 -> 1.4.1
|
||||
|
||||
# Feature opt-in en framework
|
||||
/version cpp/framework minor "anade cfg.auto_dockspace para overlay de paneles flotantes"
|
||||
# -> 1.1.0 -> 1.2.0
|
||||
|
||||
# Feature en app C++
|
||||
/version apps/chart_demo minor "anade tab radar chart con datos sinteticos"
|
||||
# -> 1.2.0 -> 1.3.0
|
||||
|
||||
# Bug fix en app de proyecto
|
||||
/version projects/fn_monitoring/apps/registry_dashboard patch "fix tooltip que mostraba duration_ms en segundos"
|
||||
# -> 0.4.1 -> 0.4.2
|
||||
|
||||
# Breaking en app: cambia schema de su BBDD propia
|
||||
/version apps/kanban major "cards.assignee_id pasa a ser TEXT[] (era TEXT); requiere migracion 008"
|
||||
# -> 1.0.0 -> 2.0.0
|
||||
```
|
||||
|
||||
## Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| Editar `version:` a mano sin `## Capability growth log` | Drift entre version y log; nadie sabe que cambio. |
|
||||
| Bumpear major en app por refactor interno | Confunde al usuario; refactor es patch. |
|
||||
| Patch para feature visible | Usuario no se entera que esta disponible. |
|
||||
| Reason "cambios varios" / "mejoras" | Inutil para auditar. Una frase concreta. |
|
||||
| Bump de app sin tocar codigo de la app (solo dep) | Bump va al modulo, no a la app. |
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "Vista cross-cutting de issues + flows. Subcomandos: today, weekly, search, dashboard. Mezcla los dos universos en una lista priorizable."
|
||||
---
|
||||
|
||||
# /work — Vista cross-cutting issues + flows
|
||||
|
||||
Issues = trabajo de implementacion. Flows = casos de uso multi-app. `/work` los muestra juntos para responder "que hago ahora" sin saltar entre dos sitios.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/work today # top items prio alta + deps satisfechas (issues + flows)
|
||||
/work weekly # review semanal: closed vs planeados
|
||||
/work search "texto" # FTS sobre issues + flows + completed
|
||||
/work dashboard # JSON consumible por tab Work (issue 0102)
|
||||
```
|
||||
|
||||
## Implementacion
|
||||
|
||||
**Fase 1 (manual via Claude):**
|
||||
|
||||
El agente lee `dev/issues/*.md` + `dev/flows/*.md`, parsea frontmatter YAML, ordena por:
|
||||
|
||||
1. `priority: alta` primero.
|
||||
2. `status: pendiente` con `depends` todos `completado` (no bloqueados).
|
||||
3. Items con DoD/Acceptance >=80% (a punto de cerrar).
|
||||
4. Fecha `updated` mas reciente.
|
||||
|
||||
Imprime tabla unificada:
|
||||
|
||||
```
|
||||
KIND | ID | TITLE | PRIO | STATUS | NEXT STEP
|
||||
issue| 0099 | datahub app launcher | alta | pendiente | revisar deps
|
||||
flow | 0001 | hn-top-stories | high | pending | cerrar DoD user-facing
|
||||
issue| 0100 | migrate issue frontmatter | alta | pendiente | ejecutar pipeline
|
||||
...
|
||||
```
|
||||
|
||||
**Fase 2 (cuando 0101 dev_console exista):**
|
||||
|
||||
`./apps/dev_console/dev_console work <subcomando> $ARGS`.
|
||||
|
||||
## Subcomandos
|
||||
|
||||
### `today`
|
||||
|
||||
Filtro: `priority in (alta, media)` + `status: pendiente` + dependencias resueltas. Max 10 items. Si hay >10, prioriza `alta` y avisa "N items pendientes en cola".
|
||||
|
||||
### `weekly`
|
||||
|
||||
Git log `--since='1 week ago'` sobre `dev/issues/completed/` y `dev/flows/completed/` -> tabla de items cerrados. Comparado con `created: <esta semana>` -> ratio in/out.
|
||||
|
||||
### `search "texto"`
|
||||
|
||||
`grep -ri` sobre `dev/issues/` + `dev/flows/` (incluido completed/), filtra por title/body. Output: `path:line: match`.
|
||||
|
||||
### `dashboard`
|
||||
|
||||
Output JSON estructurado para consumo por tab Work del `registry_dashboard` (issue 0102). Estructura:
|
||||
|
||||
```json
|
||||
{
|
||||
"issues": {"pendiente": [...], "in-progress": [...], "bloqueado": [...], "completado_24h": [...]},
|
||||
"flows": [{"id": "0001", "dod_percent": 50, "user_facing_percent": 0, "...": ...}],
|
||||
"telemetry": {"calls_24h": N, "violations_24h": N, "pending_proposals": N}
|
||||
}
|
||||
```
|
||||
@@ -21,6 +21,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
||||
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
|
||||
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
|
||||
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
|
||||
| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo |
|
||||
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
|
||||
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
|
||||
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
|
||||
@@ -34,3 +35,6 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
||||
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
|
||||
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
|
||||
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
|
||||
| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 |
|
||||
| 32 | [../../dev/TAXONOMY.md](../../dev/TAXONOMY.md) | Allowlist canonica para dominios/tipos/scopes/estados/prioridades + flow patterns. Aplica a `dev/issues/` y `dev/flows/`. Issues 0100 + 0103 |
|
||||
| 33 | [project_commands.md](project_commands.md) | Slash commands por project (`.claude/commands/<project>/`) expuestos via symlink. Desde fn_registry: `/<project>:foo`. Desde el project: `/foo`. Sin colision. |
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
## Apps son sub-repos Gitea independientes — gotcha al usar worktrees
|
||||
|
||||
**Regla operativa critica** descubierta el 2026-05-18 durante implementacion del flow 0008.
|
||||
|
||||
### El gotcha
|
||||
|
||||
`apps/*/` esta en `.gitignore` del repo `fn_registry`. Cada app es **su propio repo Gitea** en `dataforge/<app_name>` con su `.git/` dentro de `apps/<app_name>/`. Esto significa:
|
||||
|
||||
- Cuando un agente trabaja en un git **worktree** del repo padre y crea `apps/<nueva_app>/`, los archivos viven SOLO en el working directory del worktree.
|
||||
- Como `apps/*/` esta gitignored en el repo padre, los archivos **no se pueden commitear** al worktree del repo padre.
|
||||
- Cuando se hace `git worktree remove --force worktrees/<slug>/`, el working directory entero se borra — **el codigo de la app desaparece**.
|
||||
|
||||
**Consecuencia**: una app creada dentro de un worktree del repo padre se pierde al limpiar el worktree salvo que se haya promovido a su propio sub-repo Gitea ANTES.
|
||||
|
||||
### El patron correcto al crear apps en worktrees
|
||||
|
||||
```bash
|
||||
# 1. Agente trabaja en worktree del repo padre
|
||||
cd /home/lucas/fn_registry/worktrees/<slug>
|
||||
|
||||
# 2. Scaffold la app via pipeline canonico
|
||||
./fn run init_cpp_app <name> # apps C++
|
||||
# o ./fn run init_jupyter_analysis ... # analysis
|
||||
# o crear apps/<name>/ a mano (Go service, etc.)
|
||||
|
||||
# 3. ANTES de salir del worktree: inicializa la app como sub-repo
|
||||
cd apps/<name>
|
||||
git init -b master
|
||||
git add -A
|
||||
git -c user.email="agent@fn_registry" -c user.name="agent" \
|
||||
commit -m "feat: initial scaffold of <name>"
|
||||
|
||||
# 4. Trabajo continua en sub-repo (commits dentro de apps/<name>/.git)
|
||||
# 5. Cerrar issue en repo padre (mv .md a completed/), commit del padre con cambios en cpp/CMakeLists.txt, etc.
|
||||
```
|
||||
|
||||
Cuando el humano corre `/full-git-push` despues del merge, el script `ensure_repo_synced_bash_infra` detecta que `apps/<name>/.git` existe + no tiene remote + crea repo Gitea en `dataforge/<name>` + pushea master.
|
||||
|
||||
### Que ESTA SI versionado en el repo padre
|
||||
|
||||
- `cpp/CMakeLists.txt` (el `if(EXISTS ...) add_subdirectory(apps/<name>) endif()`).
|
||||
- `dev/issues/completed/<NNNN>-<slug>.md` (cierre del issue).
|
||||
- `docs/capabilities/*.md` si la app aporta a un capability group.
|
||||
- `dev/feature_flags.json` si introduce flags.
|
||||
|
||||
Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps/<name>/.git` independiente.
|
||||
|
||||
### Sintomas de la perdida
|
||||
|
||||
Si limpias el worktree y luego corres `ls apps/<name>/`, devuelve "No such file or directory" pese a que el issue aparece cerrado en `dev/issues/completed/`. **Patron** = scaffold sin sub-repo init = trabajo perdido.
|
||||
|
||||
### Recovery si pasa
|
||||
|
||||
1. Re-crear worktree desde master.
|
||||
2. Re-spawn agente con instruccion explicita: **`git init` dentro de la app antes de terminar**.
|
||||
3. NO eliminar el worktree hasta confirmar que `apps/<name>/.git` esta inicializado con al menos un commit.
|
||||
|
||||
### Aplica tambien a analysis
|
||||
|
||||
`analysis/*/` y `projects/*/analysis/*/` siguen mismo patron (cada analysis es repo Gitea). El pipeline `init_jupyter_analysis_bash_pipelines` ya hace `git init` automatico — por eso no hubo perdidas alli. Las apps C++/Go scaffolded a mano NO inicializan el sub-repo automaticamente — es responsabilidad del agente.
|
||||
|
||||
### Lo que aprende `parallel-fix-issues`
|
||||
|
||||
El template del prompt de cada agente DEBE incluir la instruccion:
|
||||
|
||||
> "Si tu issue crea una app nueva en `apps/<name>/`, inicializa el sub-repo (`cd apps/<name> && git init -b master && git add -A && git commit ...`) antes de terminar. Sin esto, `apps/*` esta gitignored y el codigo se perdera cuando el orquestador limpie el worktree."
|
||||
|
||||
Aplicar este parrafo al template del skill — ver `.claude/skills/parallel-fix-issues/SKILL.md` (o equivalente).
|
||||
|
||||
### Relacion con otras reglas
|
||||
|
||||
- [[apps_tbd]] — TBD en apps, esta regla complementa con el patron de sub-repo init.
|
||||
- [[artefactos]] — apps son artefactos, esta regla especifica gotcha de su sub-repo.
|
||||
- [[apps_vs_functions]] — apps en `apps/`, esta regla refuerza por que apps/* gitignored.
|
||||
@@ -0,0 +1,102 @@
|
||||
## Bucle autonomo (`fn-orquestador` + `/autonomous-task`) — issue 0069
|
||||
|
||||
`fn-orquestador` recorre el ciclo reactivo (CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR) sin intervencion humana, hasta convergencia (suite verde), estancamiento (no progreso N iteraciones), timeout, o tope de iteraciones. Trabaja SIEMPRE en sandbox `auto/<issue>`, NUNCA merge a master.
|
||||
|
||||
### Cuando se invoca
|
||||
|
||||
- Skill `/autonomous-task <issue_id>` (humano lanza explicitamente).
|
||||
- Cron / dag_engine (`schedule:` en YAML; planificable, no implementado por defecto).
|
||||
- NO se invoca como reaccion a hooks ni a fallos de tests "en caliente". Siempre tarea explicita.
|
||||
|
||||
### Reglas duras
|
||||
|
||||
1. **Sandbox obligatorio**: rama `auto/<issue_id>-<slug>`. Si la rama existe -> reset hard contra master y reanudar. NUNCA commits a master, NUNCA push --force-with-lease a master.
|
||||
2. **Paths protegidos**: respetar `dev/autonomous_protected_paths.json` exactamente. Cualquier intento de modificar un path protegido aborta la iteracion y registra `task_runs.status='aborted_protected_path'`.
|
||||
3. **Filtro de proposals auto-aplicables**: el orquestador SOLO aplica proposals que cumplen:
|
||||
- `kind in (bug_fix, e2e_check_add, doc_update, capability_tag_add)` -> auto-aplicable.
|
||||
- `kind in (new_function, deprecate_function, refactor, schema_change)` -> NO auto-aplicable (queda `pending` para humano).
|
||||
- `priority in (low, medium)` -> auto-aplicable. `high|critical` -> requiere humano salvo override `--allow-high`.
|
||||
4. **Watchdog**: si la metrica de progreso (`checks_pass / checks_total`) no sube en `N=3` iteraciones consecutivas -> abort. Registrar `task_runs.status='stalled'`.
|
||||
5. **Tiempo**: cada `task_run` con timeout default 30 min. Override con `--timeout-min N` hasta max 4h.
|
||||
6. **Idempotencia**: re-ejecutar `/autonomous-task <id>` sobre la misma issue reanuda desde la ultima iteracion exitosa, NO reinicia desde cero (lookup en `task_runs` por `issue_id`).
|
||||
7. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary}`. El humano puede leer el log entero para auditar.
|
||||
8. **No self-modification**: orquestador NUNCA modifica `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`. Reforzado en `autonomous_protected_paths.json`.
|
||||
9. **NUNCA paths absolutos fuera del worktree**. Refuerzo del piloto 1 (2026-05-15): el orquestador uso `/home/lucas/fn_registry/bash/functions/...` para fixear hooks bash y contamino el repo principal. Solucion correcta: fix vive solo en el worktree. Post-cada-iteracion: `git -C <main_repo> status --short` debe permanecer igual al baseline; cualquier diff = `status=sandbox_breach` -> ABORT.
|
||||
10. **Pre-commit hooks compartidos**. Worktrees comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecutara la version de main. Si el hook bloquea progreso por bug en main: aplica el fix EN EL WORKTREE (commit en auto/*); si el bug del hook excede scope: `git commit --no-verify` para ESE commit con `task_runs.events_json[].decision="skip_hook"` + razon. NO editar main.
|
||||
|
||||
### Sub-repos vs worktree padre
|
||||
|
||||
Cuando el issue toca `app.md` o codigo dentro de `apps/<name>/`, `projects/<p>/apps/<name>/`, `cpp/apps/<name>/`, o `analysis/<a>/` — estos directorios son **sub-repos Gitea independientes** y estan `.gitignore`d en el repo padre `fn_registry` (regla `apps_subrepo.md`). El orquestador:
|
||||
|
||||
- **Crea worktree padre** `auto/<issue>` en `/tmp/fn_orq_<issue>_<ts>/` por protocolo, **pero no escribe alli** porque los cambios no se versionan en el padre.
|
||||
- **Opera DIRECTAMENTE en el sub-repo** de la app/analysis target. Branch `auto/<issue>-<slug>` se crea dentro de `apps/<name>/.git`, NO en el padre.
|
||||
- **PR draft sale al sub-repo** en `dataforge/<name>` (NO a `dataforge/fn_registry`). Humano revisa+mergea en el sub-repo.
|
||||
- **Worktree padre queda vacio** y se limpia normal con `git worktree remove` al terminar.
|
||||
|
||||
Validado en piloto 0120 (`add_e2e_check` sobre `chart_demo`): PR creado en `dataforge/chart_demo/pulls/1`, sanity check del main repo `fn_registry` confirmo cero contaminacion.
|
||||
|
||||
Si el issue toca AMBOS lados (codigo del registry padre + app de sub-repo), el orquestador commitea separado: cambios del padre en `auto/<issue>` (worktree padre), cambios de la app en `auto/<issue>-<slug>` (sub-repo). Dos PRs draft. Humano coordina merge.
|
||||
|
||||
### Gitea API vs `gh`
|
||||
|
||||
Pre-condicion `gh auth status` es smoke check (target github.com). Mecanismo real de PR es `curl` a Gitea API:
|
||||
|
||||
```bash
|
||||
GITEA_TOKEN=$(pass gitea/dataforge-git-token | head -n1)
|
||||
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"...","head":"auto/<issue>-<slug>","base":"master","draft":true,"body":"..."}' \
|
||||
"https://gitea-.../api/v1/repos/dataforge/<repo>/pulls"
|
||||
```
|
||||
|
||||
Validado en pilotos 0076 y 0120.
|
||||
|
||||
### Estructura task_run
|
||||
|
||||
Migration `fn_operations/migrations/006_task_runs.sql`. Campos minimos: `id`, `issue_id`, `branch`, `started_at`, `finished_at`, `status` (`running|done|failed|aborted_protected_path|stalled|timeout`), `iterations`, `checks_pass`, `checks_fail`, `proposals_applied_json`, `proposals_skipped_json`, `events_json`, `final_diff_sha`.
|
||||
|
||||
### Fases por iteracion
|
||||
|
||||
```
|
||||
loop:
|
||||
1. fn-constructor (Read+Edit+Write+Bash limitados) - aplica fix segun ultima proposal seleccionada
|
||||
2. fn-executor - corre build + tests + smoke
|
||||
3. fn-recopilador - audita operations.db de la app
|
||||
4. fn-analizador - corre e2e_checks (registra e2e_runs)
|
||||
5. SI todos los checks pasan -> commit + push rama + abre PR. status=done. exit.
|
||||
6. SI no progreso N iteraciones -> abort. status=stalled.
|
||||
7. fn-mejorador - crea proposals desde fallos
|
||||
8. orquestador filtra proposals auto-aplicables -> selecciona la primera -> goto 1.
|
||||
```
|
||||
|
||||
### Output al humano
|
||||
|
||||
```
|
||||
=== /autonomous-task 0068 ===
|
||||
task_run_id: run_e2e_a1b2c3
|
||||
branch: auto/0068-e2e-validation
|
||||
iterations: 4
|
||||
status: done
|
||||
checks_pass: 8/8
|
||||
proposals_applied: 3 (run_e2e_run_001, run_e2e_run_002, run_e2e_run_003)
|
||||
proposals_skipped: 1 (refactor — needs human review)
|
||||
PR: https://gitea.../pulls/42
|
||||
```
|
||||
|
||||
### Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| Mergear `auto/<issue>` a master sin PR + humano | Salta gate, riesgo de regresion |
|
||||
| Auto-aplicar proposal `kind=refactor` | Cambios sistemicos requieren revision |
|
||||
| Modificar `go.sum`, `package-lock.json`, `uv.lock` | Cambios de deps requieren CVE/license review |
|
||||
| Bucle infinito sin watchdog | Coste descontrolado de tokens |
|
||||
| Borrar archivos sin backup en `task_runs.events_json` | Pierde auditoria |
|
||||
| Override de paths protegidos via env var | Bypass de seguridad |
|
||||
|
||||
### Relacion con otras reglas
|
||||
|
||||
- [[e2e_validation]] — fn-analizador (fase 4) lee el contrato `e2e_checks` que el orquestador usa como gate.
|
||||
- [[apps_tbd]] — el orquestador opera en rama `auto/*`, no exenta de TBD.
|
||||
- [[feature_flags]] — si el fix no esta terminado, el orquestador puede meterlo detras de flag OFF antes de PR.
|
||||
- [[registry_calls]] — toda invocacion del orquestador y sub-agentes pasa por MCP/`fn run`/heredoc canonico, registrada en call_monitor.
|
||||
+233
-12
@@ -20,14 +20,14 @@ Razones:
|
||||
|
||||
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
|
||||
|
||||
### 1. Ubicacion
|
||||
### 1. Ubicacion (issue 0096 estandarizada)
|
||||
|
||||
| Caso | Donde vive |
|
||||
|---|---|
|
||||
| App independiente | `cpp/apps/<nombre>/` |
|
||||
| App independiente | `apps/<nombre>/` |
|
||||
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
|
||||
|
||||
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
|
||||
NUNCA en `cpp/apps/<nombre>/` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
|
||||
|
||||
### 2. Estructura minima
|
||||
|
||||
@@ -84,6 +84,7 @@ Plantilla minima para apps C++:
|
||||
name: <name>
|
||||
lang: cpp
|
||||
domain: <gfx|tui|tools|infra|...>
|
||||
version: 0.1.0 # semver per-app, bumped via /version
|
||||
description: "Frase corta — lo que hace y por que existe."
|
||||
tags: [imgui, ...] # si es service, anadir 'service'
|
||||
uses_functions: # IDs del registry — el indexer NO deduce C++
|
||||
@@ -102,6 +103,7 @@ Reglas:
|
||||
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
|
||||
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
|
||||
- `repo_url` apunta al sub-repo en Gitea (ver §6).
|
||||
- `version`: semver per-app. Baseline `0.1.0` para apps nuevas. Bump obligatorio via `/version apps/<name> {major|minor|patch} "<reason>"` cuando `/fix-issue` toque codigo de la app. Trazabilidad humana en seccion `## Capability growth log` al final del `app.md` (una linea por bump). Ver `.claude/commands/version.md`.
|
||||
|
||||
### 5. Registro en `cpp/CMakeLists.txt`
|
||||
|
||||
@@ -189,20 +191,105 @@ WMs). Activado por defecto, sin opt-in:
|
||||
con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame).
|
||||
2. **Per-frame viewport sync** al inicio del main loop — cubre viewports
|
||||
secundarios (paneles drag-out) que la backend crea dinamicamente.
|
||||
3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE`
|
||||
/ `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras
|
||||
el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`,
|
||||
replicando el contrato del title-bar drag native (DefWindowProc bloquea
|
||||
el hilo, DWM compositor mueve el framebuffer existente).
|
||||
3. **Win32 WndProc subclass per HWND** (`#ifdef _WIN32`) — observa
|
||||
`WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada
|
||||
drag. El subclass se instala en la ventana principal Y en cada HWND
|
||||
secundario que el backend de ImGui crea cuando un panel se arrastra fuera
|
||||
del main (escaneo per-frame de `pio.Viewports`). Mientras el bracket esta
|
||||
abierto en CUALQUIER HWND propio, el main loop SKIPEA `render_fn` +
|
||||
`glfwSwapBuffers` globalmente, replicando el contrato del title-bar drag
|
||||
native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer
|
||||
existente). El flag `g_in_sizemove` es global a proposito: una sola
|
||||
sesion de sizemove externo pausa todo el render para que ninguna ventana
|
||||
compita con el OS.
|
||||
|
||||
Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases:
|
||||
Estado del subclass:
|
||||
- `g_subclassed` = `unordered_map<HWND, WNDPROC>`. Chain a la proc
|
||||
original via `CallWindowProcW`.
|
||||
- `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa).
|
||||
- Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada
|
||||
`pio.Viewports[i]->PlatformHandle` nuevo.
|
||||
- `uninstall_sizemove_subclass_all()` restaura cada HWND al exit.
|
||||
|
||||
#### Iconified main no pierde paneles flotantes (2026-05-16)
|
||||
|
||||
El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO
|
||||
el frame loop. Con multi-viewport activo eso significa que
|
||||
`ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de
|
||||
refrescar los viewports secundarios — los floating panels aparecen congelados
|
||||
o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta
|
||||
viewports secundarios primero; si hay alguno, fall-through al frame normal
|
||||
(la swap del main HWND minimizado es harmless, los contexts GL secundarios
|
||||
siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`.
|
||||
|
||||
#### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16)
|
||||
|
||||
WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`):
|
||||
|
||||
- `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` +
|
||||
`PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo.
|
||||
- `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/
|
||||
BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite
|
||||
`PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo.
|
||||
|
||||
Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a
|
||||
cada viewport flotante porque el subclass per-frame ya cubre todos los HWND.
|
||||
El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa
|
||||
render → cero jitter automatico, mismo contrato que el title-bar drag.
|
||||
|
||||
**Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut
|
||||
UI. Aceptable porque Alt-modifier en clicks UI es muy raro.
|
||||
|
||||
#### Title-bar-only move para ImGui windows (2026-05-16)
|
||||
|
||||
`fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico
|
||||
para viewports secundarios: un viewport flotante = OS window borderless con
|
||||
UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas
|
||||
arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport
|
||||
entero, el OS window sigue al cursor sin modifier. Con el flag, floating
|
||||
panels obedecen el contrato "solo header arrastra" (igual que main que tiene
|
||||
title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido
|
||||
antes por el subclass).
|
||||
|
||||
#### Test observability — `fn::internal::*` (2026-05-16)
|
||||
|
||||
Counters monotonicos para validar el subclass desde tests headless,
|
||||
zero-cost en prod:
|
||||
|
||||
```cpp
|
||||
namespace fn::internal {
|
||||
int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE
|
||||
int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido
|
||||
int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido
|
||||
int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN
|
||||
void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests
|
||||
}
|
||||
```
|
||||
|
||||
En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan
|
||||
pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el
|
||||
modal de Windows. Path real en prod sigue posteandolos.
|
||||
|
||||
Tests: `apps/altsnap_jitter_test/` corre seis fases:
|
||||
- `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta
|
||||
`vp->Pos` sigue OS dentro de 1px.
|
||||
- `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` +
|
||||
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta
|
||||
que `render()` no se llama durante el bracket.
|
||||
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el
|
||||
HWND principal, asserta que `render()` no se llama durante el bracket.
|
||||
- `p3.secondary` (Windows): fuerza viewport secundario
|
||||
(`ConfigViewportsNoAutoMerge=true`), localiza su HWND y repite el bracket
|
||||
sobre el. Valida que el subclass per-viewport tambien pausa el render.
|
||||
- `p4.minimize` (Windows): state machine 4 steps — captura
|
||||
`IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow +
|
||||
glfwRestoreWindow`. Asserta los 3 estados vivos y `renders_iconified > 0`.
|
||||
- `p5.alt_rmb` (Windows): `set_force_alt_for_test(true)` +
|
||||
`SendMessage(WM_RBUTTONDOWN)` sincrono mismo-hilo. Asserta
|
||||
`alt_rmb_resize_count` incrementa.
|
||||
- `p6.alt_lmb` (Windows): mismo patron para `WM_LBUTTONDOWN`. Asserta
|
||||
`alt_lmb_move_count` incrementa.
|
||||
|
||||
Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`.
|
||||
Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh &&
|
||||
e2e_run_cpp_windows altsnap_jitter_test`.
|
||||
|
||||
NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app
|
||||
necesita renderizar incluso durante external move (caso raro: telemetria
|
||||
@@ -261,3 +348,137 @@ de antes: `imgui.ini` es la unica fuente.
|
||||
- App headless / capture mode: `cfg.auto_layouts = false`.
|
||||
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
|
||||
`local_files/`).
|
||||
|
||||
### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16
|
||||
|
||||
Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en
|
||||
`<app_dir>/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro
|
||||
`add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si
|
||||
`WIN32` + existe `<CMAKE_CURRENT_SOURCE_DIR>/appicon.ico`, genera un
|
||||
`<target>_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con
|
||||
`IDI_ICON1 ICON "<path>"` y lo anade a `add_executable`. El compilador RC
|
||||
(`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`)
|
||||
lo enlaza al `.exe` como recurso `.rsrc`.
|
||||
|
||||
Verificar: `x86_64-w64-mingw32-objdump -h <app>.exe | grep rsrc` debe
|
||||
mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara
|
||||
`LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`).
|
||||
|
||||
#### Crear `.ico` para una app nueva
|
||||
|
||||
Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de
|
||||
`https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`,
|
||||
`bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor
|
||||
legibilidad a 16/24px.
|
||||
|
||||
Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor
|
||||
sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una
|
||||
linea por app:
|
||||
|
||||
```python
|
||||
from infra import generate_app_icon
|
||||
generate_app_icon(
|
||||
phosphor_icon_name="chart-bar",
|
||||
accent_hex="#0ea5e9",
|
||||
out_ico_path="apps/chart_demo/appicon.ico",
|
||||
)
|
||||
```
|
||||
|
||||
Mapping vive en el frontmatter de cada `app.md` C++:
|
||||
|
||||
```yaml
|
||||
description: "Frase corta de 1 linea — que hace la app y por que existe."
|
||||
icon:
|
||||
phosphor: "chart-bar"
|
||||
accent: "#0ea5e9"
|
||||
```
|
||||
|
||||
### Trio obligatorio: description + icon.phosphor + icon.accent
|
||||
|
||||
**REGLA DURA:** TODA app C++/imgui declara los **3 campos JUNTOS** en su `app.md`:
|
||||
1. `description:` (string corta, 1 linea) — texto que el `app_hub_launcher` muestra en la tarjeta y que el dashboard usa para tooltips.
|
||||
2. `icon.phosphor:` (nombre del glyph Phosphor sin sufijo `-fill`) — glyph del icono.
|
||||
3. `icon.accent:` (hex `#rrggbb`) — color del fondo redondeado del icono **Y** color del boton/border de la tarjeta en `app_hub_launcher`.
|
||||
|
||||
Los 3 se consumen como un set unico: el icono visual + el texto + el color de marca de la app. Una app sin descripcion aparece como tarjeta gris sin texto; sin `icon:` cae al default (`app-window` slate); sin accent el boton del hub aparece blanco. **Documentar uno sin los otros es bug**, no estilo.
|
||||
|
||||
### Refrescar el App Hub tras editar el trio
|
||||
|
||||
`app_hub_launcher` cachea iconos (PNG) y manifest (TSV) al arrancar. Cambiar `description`/`icon.*` en un `app.md` requiere regenerar ambos sidecars + relanzar el hub. Pipeline canonico:
|
||||
|
||||
```bash
|
||||
./fn run refresh_app_hub # icons + manifest + restart hub
|
||||
./fn run refresh_app_hub --no-restart # solo regenera, util si el hub esta cerrado
|
||||
./fn run refresh_app_hub --size 128 # PNGs 128px en vez de 64
|
||||
```
|
||||
|
||||
ID: `refresh_app_hub_bash_pipelines`. Compone `export_hub_icons_py_infra` + `export_hub_manifest_py_infra` + `is_cpp_app_running_windows_bash_infra` + `launch_cpp_app_windows_bash_infra`.
|
||||
|
||||
Regeneracion batch via pipeline del registry — escanea `app.md`s y compone
|
||||
`generate_app_icon` por app. Anadir app nueva: declarar `icon:` en su
|
||||
`app.md` y lanzar:
|
||||
|
||||
```bash
|
||||
./fn run regenerate_app_icons # todas
|
||||
./fn run regenerate_app_icons chart_demo # solo una
|
||||
```
|
||||
|
||||
Convenciones:
|
||||
- **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`).
|
||||
- **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700
|
||||
funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.).
|
||||
- **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado.
|
||||
- **Glyph color**: siempre blanco sobre el fondo accent.
|
||||
|
||||
Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/`
|
||||
con `ls | grep <keyword>` antes de inventar — 1512 disponibles.
|
||||
|
||||
#### Re-deploy tras cambiar icono
|
||||
|
||||
```bash
|
||||
# 1. Editar icon: en apps/chart_demo/app.md y regenerar
|
||||
./fn run regenerate_app_icons chart_demo
|
||||
# (o ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" para uno suelto sin tocar app.md)
|
||||
|
||||
# 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc)
|
||||
./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build
|
||||
```
|
||||
|
||||
Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras
|
||||
desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer.
|
||||
|
||||
#### Runtime attach: taskbar + title bar + Alt+Tab (2026-05-16)
|
||||
|
||||
Embeber `.ico` en el `.exe` (windres) basta para File Explorer / shortcuts —
|
||||
pero GLFW crea su WNDCLASS sin icono, asi que la **barra de tareas**, el
|
||||
**header de la ventana** y **Alt+Tab** muestran el icono GLFW por defecto a
|
||||
menos que adjuntemos el recurso al HWND en runtime.
|
||||
|
||||
`fn::run_app` lo hace automaticamente, sin opt-in. Tras `glfwCreateWindow`:
|
||||
|
||||
```cpp
|
||||
HICON hSmall = LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(101),
|
||||
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
|
||||
GetSystemMetrics(SM_CYSMICON), LR_SHARED);
|
||||
HICON hBig = LoadImageW(..., SM_CXICON, SM_CYICON, LR_SHARED);
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); // title bar
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); // taskbar
|
||||
SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall);
|
||||
SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig);
|
||||
```
|
||||
|
||||
Resource ID `101` lo emite `add_imgui_app` en el `.rc` generado
|
||||
(`101 ICON "<app_dir>/appicon.ico"`). Si la app no tiene `appicon.ico`, el
|
||||
`.rc` no se genera, `LoadImageW` devuelve NULL y el HWND queda con el icono
|
||||
GLFW por defecto (sin error).
|
||||
|
||||
Cobertura multi-viewport: el per-frame scan de `pio.Viewports` (mismo que
|
||||
instala el sizemove subclass) tambien llama `attach_app_icon_to_hwnd` sobre
|
||||
cada HWND secundario nuevo. Floating panels dragged-out heredan el icono
|
||||
sin codigo extra en la app.
|
||||
|
||||
Cache shell: el pipeline `redeploy_cpp_app_windows` llama
|
||||
`refresh_windows_icon_cache_bash_infra` tras copiar el .exe — invoca
|
||||
`ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar
|
||||
a que detecte el cambio por timestamp. Si Explorer sigue mostrando el
|
||||
icono viejo: borrar `%LOCALAPPDATA%\IconCache.db` + reiniciar Explorer.
|
||||
|
||||
@@ -20,10 +20,18 @@ fn doctor sync # Solo drift pc_locations BD vs disco local
|
||||
fn doctor uses-functions # Solo audit imports reales vs uses_functions
|
||||
fn doctor unused # Solo funciones huerfanas del registry
|
||||
fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado)
|
||||
# + check BeginTable inline: CANDIDATE (no migrado) / MIXED (parcial) / silencio (limpio)
|
||||
|
||||
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
|
||||
```
|
||||
|
||||
`fn doctor cpp-apps` produce dos secciones:
|
||||
1. Conformance (cfg.about/log, fn::run_app, menubar, DockSpace) — una fila por app imgui.
|
||||
2. BeginTable migration (issue 0081) — solo apps con `ImGui::BeginTable` inline:
|
||||
- `CANDIDATE`: N tablas inline sin `data_table_cpp_viz` en uses_functions. Considerar migracion.
|
||||
- `MIXED`: N tablas inline con `data_table_cpp_viz` ya declarado. Migracion parcial OK.
|
||||
- silencio: 0 BeginTable inline (limpio o completamente migrado).
|
||||
|
||||
### Mapeo subcomando → funcion del registry
|
||||
|
||||
| Subcomando | Funcion |
|
||||
@@ -33,7 +41,8 @@ fn doctor --json # Salida JSON (cualquier subcomando) — para agentes
|
||||
| `sync` | `pc_locations_drift_go_infra` |
|
||||
| `uses-functions` | `audit_uses_functions_go_infra` |
|
||||
| `unused` | `find_unused_functions_go_infra` |
|
||||
| `cpp-apps` | `audit_cpp_apps_go_infra` |
|
||||
| `cpp-apps` (conformance) | `audit_cpp_apps_go_infra` |
|
||||
| `cpp-apps` (table migration) | `audit_cpp_table_migration_go_infra` (inline en `audit_cpp_apps.go`) |
|
||||
|
||||
Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente.
|
||||
|
||||
@@ -64,6 +73,8 @@ Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable
|
||||
| `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio |
|
||||
| `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` |
|
||||
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
|
||||
| cpp-apps BeginTable `CANDIDATE` | App tiene N `ImGui::BeginTable` sin migrar. Abrir rama TBD, reemplazar tablas por `data_table::render()` via `fn_table_viz`, añadir `data_table_cpp_viz` a `uses_functions` en `app.md` |
|
||||
| cpp-apps BeginTable `MIXED` | Migracion parcial en curso. Continuar wave por wave hasta que no queden BeginTable inline |
|
||||
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
|
||||
|
||||
### Para agentes
|
||||
|
||||
@@ -28,3 +28,26 @@ 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)
|
||||
|
||||
### Bloque `service:` obligatorio (issue 0105)
|
||||
|
||||
Toda app con `tag: service` declara el bloque `service:` en su frontmatter. El indexer lo persiste en columnas dedicadas de `apps` + tabla `service_targets`. Consumido por `services_api`/`services_monitor` (issue 0106) y por `fn doctor services-spec`.
|
||||
|
||||
```yaml
|
||||
service:
|
||||
port: 8484 # null si no expone HTTP (stdio, daemon sin API)
|
||||
health_endpoint: /api/databases # ruta GET, 2xx/3xx = sano; null si no aplica
|
||||
health_timeout_s: 3
|
||||
systemd_unit: sqlite_api.service # obligatorio si runtime empieza con `systemd-`
|
||||
systemd_scope: user # user|system|null (docker-compose)
|
||||
restart_policy: always # always|on-failure|none
|
||||
runtime: systemd-user # systemd-user|systemd-system|docker-compose|stdio|manual
|
||||
pc_targets: # >=1, pc_id de pc_locations
|
||||
- aurgi-pc
|
||||
- home-wsl
|
||||
is_local_only: false # true => no se monitoriza por SSH (siempre local)
|
||||
```
|
||||
|
||||
Validacion: `fn doctor services-spec` (`functions/infra/audit_services_spec.go`). Hoy 11/11 services con bloque completo.
|
||||
|
||||
**Gotcha critico:** usar `Restart=always` (no `on-failure`) en el unit systemd. Un `SIGTERM` limpio es exit success → `on-failure` NO reinicia y el service se queda muerto silenciosamente. `sqlite_api.service` cayo 20h asi el 2026-05-17.
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`).
|
||||
## ids_naming — formato predictible
|
||||
|
||||
Nombres de funciones en snake_case. Tipos en PascalCase para Go.
|
||||
IDs: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). Predictibilidad alta -> Claude descubre por fuzzy match sin lookup. Issue 0087.
|
||||
|
||||
### Reglas
|
||||
|
||||
1. **snake_case**: `[a-z0-9_]+`. Nada de PascalCase, kebab-case, dot.notation.
|
||||
2. **Verbo obligatorio**: al menos un token del `name` debe ser un verbo de accion. El verbo puede ir delante (`get_user`) o detras (`user_lookup`). Ejemplos validos: `filter_slice`, `bank_login`, `metabase_get_dashboard`, `redeploy_cpp_app`. Invalidos: `slice` (sustantivo solo), `user` (sustantivo solo), `data` (sustantivo solo).
|
||||
3. **Dominio canonico**: el `domain` debe estar en la lista canonica (ver `mcp__registry__fn_list_domains`). Crear dominio nuevo solo si el bucket es claramente distinto y se anade en el mismo turno a CLAUDE.md.
|
||||
4. **Tipos en PascalCase Go**: `ResultGoCore`, `ErrorGoCore`. Aplica solo al codigo Go; el ID en el registry sigue siendo snake_case (`result_go_core`).
|
||||
|
||||
### Verbos canonicos (allowlist)
|
||||
|
||||
Lista no exhaustiva pero cubre la mayoria. Anadir aqui (y al validator en `apps/registry_mcp/naming.go`) cuando se introduzca un verbo nuevo recurrente.
|
||||
|
||||
`get, set, list, find, search, show, read, load, fetch, scan, query, lookup, parse, format, encode, decode, marshal, unmarshal, serialize, deserialize, validate, check, ensure, verify, audit, diagnose, test, match, filter, map, reduce, sort, group, count, sum, aggregate, compute, calculate, score, rank, cluster, classify, detect, init, create, make, build, generate, scaffold, install, setup, configure, register, add, insert, append, prepend, update, upsert, modify, edit, patch, replace, delete, remove, clear, drop, prune, clean, copy, move, rename, sync, clone, extract, inject, import, export, send, post, put, call, dispatch, exec, run, launch, start, stop, kill, restart, redeploy, deploy, open, close, connect, disconnect, login, logout, authenticate, enable, disable, toggle, lock, unlock, propose, promote, deprecate, approve, reject, emit, render, draw, paint, serve, host, pull, push, checkout, commit, tag, merge, rebase, watch, monitor, observe, log, trace, profile, benchmark, snapshot, backup, restore, archive, compress, decompress, hash, encrypt, decrypt, sign, taskkill, recopile, vault, propose, apply, gather, collect, fold, head, tail, take, drop, slice, chunk, batch, debounce, throttle, retry, await, sleep, ping, kill, prime, warm, refresh, invalidate, reload, reset, rollback, fork, spawn, daemon, observe, plot, draw, capture, replay, recopilate`
|
||||
|
||||
### Excepciones
|
||||
|
||||
- **Operadores matematicos/estadisticos** ampliamente reconocidos por acronimo: `sma`, `ema`, `rsi`, `vwap`, `adx`. Validator hace allowlist explicita.
|
||||
- **Tipos** (entity_type `type`): no requieren verbo. Validator lo salta cuando `kind=type`.
|
||||
- **Components** (`kind: component`): nombre describe artefacto UI (`button_primary`, `chat_panel`). Permite forma `<noun>_<modifier>`. Validator salta el check de verbo si `kind=component`.
|
||||
|
||||
### Validator
|
||||
|
||||
`mcp__registry__fn_create_function` ejecuta el validator antes de escribir archivos. Rechaza con error si:
|
||||
- name no es snake_case.
|
||||
- name no contiene verbo (excepto component/type).
|
||||
- domain no esta en lista canonica.
|
||||
|
||||
Error tipico:
|
||||
```
|
||||
naming: name "slice" lacks action verb. Add verb prefix/suffix (e.g. filter_slice, slice_window). See .claude/rules/ids_naming.md.
|
||||
naming: domain "bizops" not in canonical list (core, infra, finance, ...). Add it to CLAUDE.md and rules first.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
## Slash commands por project (namespaced)
|
||||
|
||||
Cada `projects/<p>/` puede tener su propio `.claude/commands/*.md`. Para invocarlos desde la raiz de `fn_registry` sin que pisen los comandos globales, se exponen via **symlink namespaced** en `fn_registry/.claude/commands/<project>/`.
|
||||
|
||||
### Patron canonico
|
||||
|
||||
```
|
||||
projects/aurgi/.claude/commands/foo.md # archivo real (viaja con el sub-repo del project)
|
||||
fn_registry/.claude/commands/aurgi -> symlink -> ../../projects/aurgi/.claude/commands
|
||||
```
|
||||
|
||||
Resultado:
|
||||
|
||||
| cwd | Invocacion |
|
||||
|---|---|
|
||||
| `cd projects/aurgi && claude` | `/foo` (sin namespace) |
|
||||
| `cd fn_registry && claude` | `/aurgi:foo` (namespaced, no colisiona con `/foo` global) |
|
||||
|
||||
Subdirs dentro de `.claude/commands/` se exponen como namespace en el slash command. Por eso `aurgi/foo.md` -> `/aurgi:foo`.
|
||||
|
||||
### Como anadir un project nuevo
|
||||
|
||||
1. `mkdir -p projects/<p>/.claude/commands/`.
|
||||
2. Crear `<comando>.md` con frontmatter `description:` + cuerpo.
|
||||
3. Symlink: `ln -sf ../../projects/<p>/.claude/commands /home/egutierrez/fn_registry/.claude/commands/<p>`.
|
||||
4. Versionar el `.claude/commands/` del project en su propio sub-repo (NO en fn_registry — projects estan gitignored).
|
||||
5. Versionar SOLO el symlink en fn_registry (`git add .claude/commands/<p>`).
|
||||
|
||||
### Reglas
|
||||
|
||||
- Cada project mantiene autonomia: sus commands viajan con el sub-repo y funcionan tanto en `cd projects/<p>` como desde la raiz.
|
||||
- El symlink en fn_registry da acceso global con namespace — sin colision con commands del registry.
|
||||
- NO duplicar contenido: archivo real solo en `projects/<p>/.claude/commands/`. fn_registry solo guarda el symlink.
|
||||
- Si el project se mueve/elimina, borrar el symlink en fn_registry.
|
||||
|
||||
### Listado actual
|
||||
|
||||
| Project | Symlink | Commands disponibles desde fn_registry |
|
||||
|---|---|---|
|
||||
| aurgi | `.claude/commands/aurgi` | `/aurgi:aumentar_task`, `/aurgi:contexto_aurgi`, `/aurgi:anadir_contexto_aurgi` |
|
||||
|
||||
Anadir filas aqui al introducir un project nuevo con commands.
|
||||
|
||||
### Catalogo dinamico
|
||||
|
||||
Para listado en tiempo real (sin tener que actualizar esta tabla a mano): `/commands` escanea `.claude/commands/` recursivo y agrupa por namespace. Filtros: `/commands <substring>`, `/commands --ns <ns>`, `/commands --json`.
|
||||
|
||||
### Gotchas
|
||||
|
||||
- Claude Code lista los commands disponibles al inicio de sesion. Si un symlink apunta a un directorio inexistente, los commands no aparecen — verificar con `ls -L .claude/commands/<project>/`.
|
||||
- El namespace usa el nombre del subdirectorio (`aurgi/`), no del project en `projects/`. Mantenerlos iguales para evitar confusion.
|
||||
- Los commands del project se ejecutan con el cwd de la sesion actual. Un `/aurgi:aumentar_task` invocado desde `fn_registry/` corre con cwd `fn_registry/` — paths relativos en el `.md` deben asumir esto (siempre usar paths relativos al repo, ej. `projects/aurgi/vaults/...`).
|
||||
@@ -140,7 +140,7 @@ Cobertura por capa, no todas activas a la vez:
|
||||
### Que NO se monitoriza
|
||||
|
||||
- Funcion Go/C++ llamada internamente por app ya compilada.
|
||||
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
|
||||
- Funcion ejecutada por systemd timer / cron / dag_engine **step `command:`** (no `function:`) sin pasar por `fn run`. Nota: dag_engine steps con `function:` SI quedan trazados — el executor invoca `fn run <id>` y guarda `function_id` en `dag_step_results`.
|
||||
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
|
||||
- Service de produccion recibiendo HTTP.
|
||||
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Append a one-liner [[fn_id]] — purpose to MEMORY.md after fn-constructor
|
||||
# creates a new registry function. Idempotent: skips if id already present.
|
||||
# Used by /fn_claude step 5b (issue 0087, pieza 6).
|
||||
#
|
||||
# Usage: append_fn_to_memory.sh <fn_id> "<one-line purpose>"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FN_ID="${1:-}"
|
||||
PURPOSE="${2:-}"
|
||||
|
||||
if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then
|
||||
echo "usage: append_fn_to_memory.sh <fn_id> <purpose>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
MEM_DIR="${CLAUDE_MEMORY_DIR:-/home/lucas/.claude/projects/-home-lucas-fn-registry/memory}"
|
||||
MEM_FILE="$MEM_DIR/MEMORY.md"
|
||||
|
||||
[ -d "$MEM_DIR" ] || { echo "memory dir missing: $MEM_DIR" >&2; exit 1; }
|
||||
[ -f "$MEM_FILE" ] || { echo "MEMORY.md missing: $MEM_FILE" >&2; exit 1; }
|
||||
|
||||
# Per-function reference file slug
|
||||
SLUG="reference_fn_${FN_ID}.md"
|
||||
REF_FILE="$MEM_DIR/$SLUG"
|
||||
|
||||
# Idempotency: if already linked in MEMORY.md, exit 0
|
||||
if grep -qF "[fn-$FN_ID]" "$MEM_FILE" 2>/dev/null; then
|
||||
echo "already in MEMORY.md: $FN_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Create reference memory file
|
||||
cat > "$REF_FILE" <<EOF
|
||||
---
|
||||
name: fn-$FN_ID
|
||||
description: Registry function $FN_ID — $PURPOSE
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
Registry function: \`$FN_ID\`
|
||||
|
||||
$PURPOSE
|
||||
|
||||
Invoke via \`./fn run $FN_ID [args]\` or \`mcp__registry__fn_run id="$FN_ID"\`. Inspect with \`mcp__registry__fn_show id="$FN_ID"\` / \`mcp__registry__fn_code id="$FN_ID"\`.
|
||||
EOF
|
||||
|
||||
# 2. Append index line to MEMORY.md
|
||||
printf -- '- [%s](%s) — %s\n' "fn-$FN_ID" "$SLUG" "$PURPOSE" >> "$MEM_FILE"
|
||||
|
||||
echo "appended: $FN_ID -> $MEM_FILE"
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# integrate-worktrees.sh — Integra branches de worktrees a master con --no-ff
|
||||
#
|
||||
# Uso: ./integrate-worktrees.sh <slug-1> <slug-2> ...
|
||||
# Ejemplo: ./integrate-worktrees.sh 0026-split-runtime 0027-prune-config-schema
|
||||
#
|
||||
# Para cada slug:
|
||||
# 1. git merge --no-ff issue/<slug> a master
|
||||
# 2. Verificar que master compila después del merge
|
||||
# 3. Si hay conflict o fallo de build, PARAR inmediatamente
|
||||
#
|
||||
# Los slugs deben pasarse en el orden correcto (waves ya resueltas).
|
||||
# NO hace push — eso lo decide el usuario.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ERROR: se necesita al menos un slug"
|
||||
echo "Uso: $0 <slug-1> <slug-2> ..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Asegurar que estamos en master
|
||||
echo "=== Cambiando a master ==="
|
||||
cd "$REPO_ROOT"
|
||||
git checkout master
|
||||
|
||||
MERGED=0
|
||||
FAILED_AT=""
|
||||
|
||||
for slug in "$@"; do
|
||||
branch="issue/${slug}"
|
||||
|
||||
echo ""
|
||||
echo "=== Integrando: ${branch} ==="
|
||||
|
||||
# Verificar que la branch existe
|
||||
if ! git show-ref --verify --quiet "refs/heads/${branch}"; then
|
||||
echo "FAIL: branch ${branch} no existe"
|
||||
FAILED_AT="$slug"
|
||||
break
|
||||
fi
|
||||
|
||||
# Merge --no-ff
|
||||
if ! git merge --no-ff "$branch" -m "merge: ${branch} — implementación paralela"; then
|
||||
echo ""
|
||||
echo "CONFLICT: merge de ${branch} tiene conflictos"
|
||||
echo "Resolver manualmente y luego continuar con los slugs restantes"
|
||||
echo ""
|
||||
echo "Para resolver:"
|
||||
echo " 1. git status (ver archivos en conflicto)"
|
||||
echo " 2. Resolver conflictos en cada archivo"
|
||||
echo " 3. git add <archivos>"
|
||||
echo " 4. git commit"
|
||||
echo ""
|
||||
echo "Slugs pendientes después de ${slug}:"
|
||||
FOUND=0
|
||||
for remaining in "$@"; do
|
||||
if [ "$FOUND" -eq 1 ]; then
|
||||
echo " - ${remaining}"
|
||||
fi
|
||||
if [ "$remaining" = "$slug" ]; then
|
||||
FOUND=1
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "MERGED: ${branch}"
|
||||
|
||||
# Verificar que master sigue compilando (si BUILD_CMD esta definido)
|
||||
if [ -n "${BUILD_CMD:-}" ]; then
|
||||
echo "--- Verificando build post-merge ($BUILD_CMD) ---"
|
||||
if ! (cd "$REPO_ROOT" && bash -c "$BUILD_CMD" 2>&1); then
|
||||
echo ""
|
||||
echo "FAIL: master no compila despues de mergear ${branch}"
|
||||
echo "Revertir con: git reset --hard HEAD~1"
|
||||
echo "Investigar el problema antes de continuar."
|
||||
FAILED_AT="$slug"
|
||||
break
|
||||
fi
|
||||
echo "OK: build post-merge exitoso"
|
||||
else
|
||||
echo "--- Build post-merge SKIPPED (BUILD_CMD no definido) ---"
|
||||
fi
|
||||
|
||||
MERGED=$((MERGED + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen de integración ==="
|
||||
echo "Mergeados: ${MERGED} de $#"
|
||||
|
||||
if [ -n "$FAILED_AT" ]; then
|
||||
echo "Falló en: ${FAILED_AT}"
|
||||
echo ""
|
||||
echo "Worktrees NO limpiados (resolver primero el fallo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Limpieza de worktrees y branches
|
||||
echo ""
|
||||
echo "=== Limpieza ==="
|
||||
for slug in "$@"; do
|
||||
path="${REPO_ROOT}/worktrees/${slug}"
|
||||
branch="issue/${slug}"
|
||||
|
||||
if [ -d "$path" ]; then
|
||||
git worktree remove "$path" 2>/dev/null && echo "REMOVED: worktree ${path}" || echo "WARN: no se pudo eliminar worktree ${path}"
|
||||
fi
|
||||
|
||||
git branch -d "$branch" 2>/dev/null && echo "DELETED: branch ${branch}" || echo "WARN: no se pudo eliminar branch ${branch}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Integración completa ==="
|
||||
echo "Master tiene ${MERGED} merges nuevos."
|
||||
echo ""
|
||||
echo "Para publicar: git push"
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# setup-worktrees.sh — Crea git worktrees para ejecución paralela de issues
|
||||
#
|
||||
# Uso: ./setup-worktrees.sh <slug-1> <slug-2> ...
|
||||
# Ejemplo: ./setup-worktrees.sh 0026-split-runtime 0027-prune-config-schema
|
||||
#
|
||||
# Cada slug genera:
|
||||
# worktrees/<slug>/ (worktree completo)
|
||||
# branch: issue/<slug>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKTREE_DIR="${REPO_ROOT}/worktrees"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ERROR: se necesita al menos un slug de issue"
|
||||
echo "Uso: $0 <slug-1> <slug-2> ..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verificar master (NO pull --rebase: rompe merges locales convirtiendolos
|
||||
# en cherry-picks contra origin/master viejo). Detectado 2026-05-18.
|
||||
echo "=== Verificando master ==="
|
||||
CURRENT_BRANCH="$(git branch --show-current)"
|
||||
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
|
||||
echo "WARN: estas en branch '${CURRENT_BRANCH}', no master. Worktrees nuevos saldran de master ref de todos modos."
|
||||
fi
|
||||
# NO auto-pull. Usuario decide sync con remote.
|
||||
|
||||
mkdir -p "$WORKTREE_DIR"
|
||||
|
||||
CREATED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
|
||||
for slug in "$@"; do
|
||||
branch="issue/${slug}"
|
||||
path="${WORKTREE_DIR}/${slug}"
|
||||
|
||||
if [ -d "$path" ]; then
|
||||
echo "SKIP: worktree ya existe: ${path}"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Verificar que la branch no existe ya
|
||||
if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
|
||||
echo "WARN: branch ${branch} ya existe, creando worktree desde ella"
|
||||
git worktree add "$path" "$branch" 2>/dev/null || {
|
||||
echo "FAIL: no se pudo crear worktree para ${slug}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
}
|
||||
else
|
||||
echo "CREATE: worktree ${path} (branch ${branch})"
|
||||
git worktree add -b "$branch" "$path" master 2>/dev/null || {
|
||||
echo "FAIL: no se pudo crear worktree para ${slug}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
}
|
||||
fi
|
||||
|
||||
CREATED=$((CREATED + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen ==="
|
||||
echo "Creados: ${CREATED}"
|
||||
echo "Existentes: ${SKIPPED}"
|
||||
echo "Fallidos: ${FAILED}"
|
||||
echo ""
|
||||
echo "=== Worktrees activos ==="
|
||||
git worktree list
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/bin/bash
|
||||
# verify-worktree.sh — Verifica build, tests y cierre de issue en un worktree.
|
||||
#
|
||||
# Uso:
|
||||
# ./verify-worktree.sh <worktree-path> [build-cmd] [test-cmd]
|
||||
#
|
||||
# Ejemplos:
|
||||
# ./verify-worktree.sh worktrees/0026-foo
|
||||
# ./verify-worktree.sh worktrees/0026-foo "go build -tags fts5 ./..." "go test -tags fts5 ./..."
|
||||
# BUILD_CMD="cmake --build cpp/build" TEST_CMD="ctest --test-dir cpp/build" ./verify-worktree.sh worktrees/0026-foo
|
||||
#
|
||||
# Resolucion de comandos (en orden de prioridad):
|
||||
# 1. Argumentos posicionales (build-cmd, test-cmd)
|
||||
# 2. Variables de entorno BUILD_CMD / TEST_CMD
|
||||
# 3. Archivo .parallel-fix-issues.yml en la raiz del worktree (claves: build, test)
|
||||
# 4. Auto-deteccion segun ficheros del proyecto:
|
||||
# - go.mod → "go build ./..." + "go test ./..."
|
||||
# - CMakeLists.txt → "cmake -S . -B build && cmake --build build" + "ctest --test-dir build"
|
||||
# - Cargo.toml → "cargo build" + "cargo test"
|
||||
# - package.json → "npm run build" + "npm test"
|
||||
# - pyproject.toml → "" + "pytest"
|
||||
# 5. Si nada se detecta, salta build/test con WARN.
|
||||
#
|
||||
# Auto-deteccion adicional: si hay go.mod, intenta extraer build tag de //go:build.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = todo OK
|
||||
# 1 = error de argumento
|
||||
# 2 = build fallo
|
||||
# 3 = tests fallaron
|
||||
# 4 = issue no cerrado (solo WARN, no falla)
|
||||
# 5 = sin commits propios
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "ERROR: se necesita el path del worktree"
|
||||
echo "Uso: $0 <worktree-path> [build-cmd] [test-cmd]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKTREE="$1"
|
||||
ARG_BUILD_CMD="${2:-}"
|
||||
ARG_TEST_CMD="${3:-}"
|
||||
|
||||
# Resolver path absoluto
|
||||
if [[ "$WORKTREE" != /* ]]; then
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKTREE="${REPO_ROOT}/${WORKTREE}"
|
||||
fi
|
||||
|
||||
if [ ! -d "$WORKTREE" ]; then
|
||||
echo "ERROR: worktree no encontrado: ${WORKTREE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLUG="$(basename "$WORKTREE")"
|
||||
echo "=== Verificando: ${SLUG} ==="
|
||||
|
||||
# --- Resolver build/test commands ---
|
||||
BUILD_CMD="${ARG_BUILD_CMD:-${BUILD_CMD:-}}"
|
||||
TEST_CMD="${ARG_TEST_CMD:-${TEST_CMD:-}}"
|
||||
|
||||
# Manifest opcional
|
||||
MANIFEST="${WORKTREE}/.parallel-fix-issues.yml"
|
||||
if [ -z "$BUILD_CMD" ] && [ -f "$MANIFEST" ]; then
|
||||
M_BUILD=$(grep -E "^build:" "$MANIFEST" 2>/dev/null | sed -E 's/^build:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
|
||||
if [ -n "$M_BUILD" ]; then BUILD_CMD="$M_BUILD"; echo "INFO: build desde manifest"; fi
|
||||
fi
|
||||
if [ -z "$TEST_CMD" ] && [ -f "$MANIFEST" ]; then
|
||||
M_TEST=$(grep -E "^test:" "$MANIFEST" 2>/dev/null | sed -E 's/^test:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
|
||||
if [ -n "$M_TEST" ]; then TEST_CMD="$M_TEST"; echo "INFO: test desde manifest"; fi
|
||||
fi
|
||||
|
||||
# Auto-deteccion
|
||||
if [ -z "$BUILD_CMD" ] || [ -z "$TEST_CMD" ]; then
|
||||
AUTO_BUILD=""
|
||||
AUTO_TEST=""
|
||||
if [ -f "${WORKTREE}/go.mod" ]; then
|
||||
# Detectar build tag
|
||||
AUTO_TAG=$(grep -rh "^//go:build " --include="*.go" "$WORKTREE" 2>/dev/null \
|
||||
| sed -E 's|^//go:build ([a-zA-Z0-9_]+).*|\1|' \
|
||||
| sort -u | head -1 || true)
|
||||
TAG_FLAG=""
|
||||
[ -n "$AUTO_TAG" ] && TAG_FLAG="-tags $AUTO_TAG"
|
||||
AUTO_BUILD="go build $TAG_FLAG ./..."
|
||||
AUTO_TEST="go test $TAG_FLAG ./..."
|
||||
echo "INFO: stack detectado: Go${TAG_FLAG:+ ($TAG_FLAG)}"
|
||||
elif [ -f "${WORKTREE}/CMakeLists.txt" ] || ls "${WORKTREE}"/cpp/CMakeLists.txt >/dev/null 2>&1; then
|
||||
CMAKE_DIR="."
|
||||
[ -f "${WORKTREE}/cpp/CMakeLists.txt" ] && [ ! -f "${WORKTREE}/CMakeLists.txt" ] && CMAKE_DIR="cpp"
|
||||
AUTO_BUILD="cmake -S ${CMAKE_DIR} -B ${CMAKE_DIR}/build -DCMAKE_BUILD_TYPE=Release && cmake --build ${CMAKE_DIR}/build -j"
|
||||
AUTO_TEST="ctest --test-dir ${CMAKE_DIR}/build --output-on-failure || true"
|
||||
echo "INFO: stack detectado: C++/CMake (dir=${CMAKE_DIR})"
|
||||
elif [ -f "${WORKTREE}/Cargo.toml" ]; then
|
||||
AUTO_BUILD="cargo build"
|
||||
AUTO_TEST="cargo test"
|
||||
echo "INFO: stack detectado: Rust"
|
||||
elif [ -f "${WORKTREE}/package.json" ]; then
|
||||
AUTO_BUILD="npm run build --if-present"
|
||||
AUTO_TEST="npm test --if-present"
|
||||
echo "INFO: stack detectado: Node"
|
||||
elif [ -f "${WORKTREE}/pyproject.toml" ] || [ -f "${WORKTREE}/setup.py" ]; then
|
||||
AUTO_BUILD="" # python normalmente no tiene build step
|
||||
AUTO_TEST="pytest"
|
||||
echo "INFO: stack detectado: Python"
|
||||
else
|
||||
echo "WARN: no se detecto stack; usar BUILD_CMD/TEST_CMD env o manifest .parallel-fix-issues.yml"
|
||||
fi
|
||||
[ -z "$BUILD_CMD" ] && BUILD_CMD="$AUTO_BUILD"
|
||||
[ -z "$TEST_CMD" ] && TEST_CMD="$AUTO_TEST"
|
||||
fi
|
||||
|
||||
# 1. Verificar commits propios
|
||||
echo ""
|
||||
echo "--- Commits propios ---"
|
||||
COMMIT_COUNT=$(cd "$WORKTREE" && git log master..HEAD --oneline 2>/dev/null | wc -l)
|
||||
if [ "$COMMIT_COUNT" -eq 0 ]; then
|
||||
echo "FAIL: sin commits propios en la branch"
|
||||
exit 5
|
||||
fi
|
||||
echo "OK: ${COMMIT_COUNT} commits desde master"
|
||||
cd "$WORKTREE" && git log master..HEAD --oneline
|
||||
|
||||
# 2. Build
|
||||
echo ""
|
||||
if [ -n "$BUILD_CMD" ]; then
|
||||
echo "--- Build ($BUILD_CMD) ---"
|
||||
if (cd "$WORKTREE" && bash -c "$BUILD_CMD" 2>&1); then
|
||||
echo "OK: build exitoso"
|
||||
else
|
||||
echo "FAIL: build fallo"
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "--- Build SKIPPED (sin comando) ---"
|
||||
fi
|
||||
|
||||
# 3. Tests
|
||||
echo ""
|
||||
if [ -n "$TEST_CMD" ]; then
|
||||
echo "--- Tests ($TEST_CMD) ---"
|
||||
if (cd "$WORKTREE" && bash -c "$TEST_CMD" 2>&1); then
|
||||
echo "OK: tests pasaron"
|
||||
else
|
||||
echo "FAIL: tests fallaron"
|
||||
exit 3
|
||||
fi
|
||||
else
|
||||
echo "--- Tests SKIPPED (sin comando) ---"
|
||||
fi
|
||||
|
||||
# 4. Issue cerrado
|
||||
echo ""
|
||||
echo "--- Cierre de issue ---"
|
||||
COMPLETED_FILES=$(cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/ 2>/dev/null | wc -l)
|
||||
if [ "$COMPLETED_FILES" -gt 0 ]; then
|
||||
echo "OK: issue movido a completed/"
|
||||
cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/
|
||||
else
|
||||
echo "WARN: no se detecto issue movido a completed/ (verificar manualmente)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== RESULTADO: ${SLUG} — OK ==="
|
||||
@@ -81,3 +81,10 @@ broken_paths.txt
|
||||
imgui.ini
|
||||
prompts/
|
||||
kotlin/functions/ui/
|
||||
|
||||
# Module versioning auto-generated headers (written by `fn index`, issue 0097)
|
||||
**/version_generated.h
|
||||
**/app_modules_generated.h
|
||||
|
||||
# Issue migration backups (0100)
|
||||
dev/issues/.backup_pre_*
|
||||
|
||||
@@ -8,6 +8,68 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## 2026-05-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Bloque `service:` en frontmatter de `app.md`** (issue 0105) — toda app con `tag: service` declara ahora `port`, `health_endpoint`, `health_timeout_s`, `systemd_unit`, `systemd_scope`, `restart_policy`, `runtime` (`systemd-user|systemd-system|docker-compose|stdio|manual`), `pc_targets[]`, `is_local_only`. 11 apps actualizadas: `sqlite_api`, `dag_engine`, `call_monitor`, `kanban`, `deploy_server`, `registry_mcp`, `registry_api`, `footprint_geo_stack`, `element_matrix_chat`, `agents_and_robots`, `services_api`.
|
||||
- **Migration `014_service_metadata.sql`** — anade 8 columnas (`service_port`, `service_health_endpoint`, `service_health_timeout_s`, `service_systemd_unit`, `service_systemd_scope`, `service_restart_policy`, `service_runtime`, `service_is_local_only`) a `apps` + tabla nueva `service_targets (app_id, pc_id, role)` con indices por `app_id` y `pc_id`.
|
||||
- **`registry.App.Service *ServiceSpec`** + parser `rawService` + escritura/lectura en `InsertApp`/`scanApps`/`Purge` (preserva `service_targets`). API publica `db.GetServicePCTargets(appID) []string`.
|
||||
- **`audit_services_spec_go_infra`** (`functions/infra/audit_services_spec.{go,md}`) — audita apps `tag: service` y reporta drift del bloque `service:` (runtime allowlist, pc_targets >=1, systemd_unit obligatorio si `runtime` empieza con `systemd-`, restart_policy en `always|on-failure|none`).
|
||||
- **`fn doctor services-spec`** — subcomando nuevo en `cmd/fn/doctor.go`. Salida tabwriter + `--json`. Hoy: `11/11 services with complete service: block`.
|
||||
- **App `services_api`** (`apps/services_api/`, issue 0106) — Go HTTP daemon en `127.0.0.1:8485`. Loop paralelo cada 15s (max 8 in-flight, timeout 20s/probe) que reconcilia esperado vs real para cada `(app, pc)` cruzado de `service_targets`. Probes locales (`systemctl is-active` + TCP dial + `http.Client`) o remotos (`ssh_exec_go_infra`). Persiste en `operations.db`: `service_state` (snapshot actual) + `service_transition` (cambios de overall append-only). Endpoints `GET /api/health`, `GET /api/services`, `POST /api/check`, `GET /api/pcs`. systemd unit `~/.config/systemd/user/services_api.service` con `Restart=always`.
|
||||
- **App `services_monitor`** (`apps/services_monitor/`, issue 0106) — frontend C++ ImGui. Polling auto cada 5s configurable + boton "Force check" (POST `/api/check`). Tabla 9-col agrupada por app: overall pill, systemd state, port + listening flag (`TI_PLUG`/`TI_PLUG_CONNECTED`), HTTP status+latency, runtime, last change age, error/note. JSON via `vendor/nlohmann/json.hpp` (copiado de data_factory). HTTP socket TCP via `http_client.{cpp,h}` (copiado de data_factory). Build linux + windows con `add_imgui_app` + ws2_32 en Win. Deploy automatico via `redeploy_cpp_app_windows`.
|
||||
- **Issues 0105 + 0106** (`dev/issues/`) — estandarizacion del bloque `service:` y app `services_monitor`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`sqlite_api.service` murio 20h sin alerta el 2026-05-17** — Raiz: el unit tenia `Restart=on-failure` y el ultimo exit fue por `SIGTERM` (limpio, no failure). systemd NO reinicia exit success. Fix: cambio a `Restart=always` + `RestartSec=5`. Reload + restart inmediato. Detectado mientras se debuggeaba `data_factory` cargando lento (raiz: data_factory llama a `sqlite_api:8484`, timeout 3s, no responde). Aplicado el mismo `Restart=always` al unit nuevo `services_api.service`.
|
||||
- **`sqlite_api/app.md` health_endpoint** — declaraba `/api/status` que devuelve 404. Cambiado a `/api/databases` (200, lista de bases registradas). Detectado por el primer ciclo del propio `services_api` que marcaba sqlite_api como `degraded`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`services_monitor` tags** — sin `service`/`services` en `tags` para evitar falso positivo en el matcher `tags LIKE '%service%'` del audit `services-spec`. La app es desktop client (frontend), no daemon.
|
||||
|
||||
## 2026-05-16
|
||||
|
||||
### Added
|
||||
|
||||
- **Panel "Logs" en `dag_engine` RunDetail** — `apps/dag_engine/frontend/src/pages/RunDetail.tsx` anade `<Paper>` final con `<Code block>` scrollable + `CopyButton` de Mantine. Helper `buildLogText(run, steps)` compone texto plano (metadata del run + por-step status/exit/duration/stdout/stderr indentado) para pegar entero al LLM sin abrir los `Collapse` del `StepTimeline`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`dag_engine` steps `function:` fallando con `error: function "<id>" not found (tried as ID and name)`** — tres DAGs nocturnos (`fn_backup` x2, `daily-registry-audit`) fallaron 2026-05-15/16 porque el binario `fn` resolvia una copia stale `apps/dag_engine/registry.db` (May 15, 262 KB) en vez del `registry.db` raiz. Raiz: el systemd unit `dag_engine.service` tiene `WorkingDirectory=apps/dag_engine/` y no exportaba `FN_REGISTRY_ROOT`; `cmd/fn/ops.go::tryOpenRegistryDB` cae al walk-up `go.mod` (devuelve `apps/dag_engine/`). Fix:
|
||||
- Borrado `apps/dag_engine/registry.db` stale (violaba `.claude/rules/db_locations.md`).
|
||||
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT`, `FN_BIN`, `PATH` (con `/usr/local/go/bin` para steps `function:` Go sin tests que invocan `go vet`), `HOME`.
|
||||
- `apps/dag_engine/executor.go`: steps `function:` exportan `FN_REGISTRY_ROOT=<root>` en env y default `dir = fnRegistryRoot` si `step.Dir`/`dag.WorkingDir` vacios. Steps `command:`/`script:` sin cambio.
|
||||
|
||||
### Added
|
||||
|
||||
- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `<exe_dir>` desplegado.
|
||||
- Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop.
|
||||
- Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256).
|
||||
- Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `<app_dir>/appicon.ico` y genera `<target>_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`).
|
||||
- Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`.
|
||||
- Convencion documentada en `.claude/rules/cpp_apps.md §11`.
|
||||
|
||||
- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame).
|
||||
- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map<HWND, WNDPROC>`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND).
|
||||
- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes.
|
||||
- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal.
|
||||
- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows.
|
||||
- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui).
|
||||
- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps/<X>/` (canonical issue 0096) ademas de `cpp/apps/<X>/` (legacy) y `projects/*/apps/<X>/`. Fix retroactivo: `./fn run compile_cpp_app <name>` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`.
|
||||
|
||||
### Notes
|
||||
|
||||
- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos.
|
||||
- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`).
|
||||
- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion.
|
||||
|
||||
## 2026-05-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[2026-05-15 23:51:43.764] [INFO] app start: altsnap_jitter_test
|
||||
[2026-05-15 23:51:44.017] [INFO] app exit
|
||||
[2026-05-15 23:52:47.933] [INFO] app start: altsnap_jitter_test
|
||||
[2026-05-15 23:52:48.135] [INFO] app exit
|
||||
@@ -0,0 +1,360 @@
|
||||
# dag_engine — Guia de uso
|
||||
|
||||
Motor de DAGs propio del fn_registry. **Scheduler oficial** del ecosistema (issue 0007a-e + flow 0001). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
|
||||
|
||||
Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Donde viven los DAGs
|
||||
|
||||
| Path | Que |
|
||||
|---|---|
|
||||
| `apps/dag_engine/dags_migrated/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). |
|
||||
| `apps/dag_engine/dags_migrated/archive/` | DAGs deshabilitados (no se cargan por el scheduler). |
|
||||
|
||||
Por defecto el systemd unit apunta a `apps/dag_engine/dags_migrated/`. Para usar otro dir, edita `~/.config/systemd/user/dag_engine.service`:
|
||||
|
||||
```ini
|
||||
ExecStart=/home/lucas/fn_registry/apps/dag_engine/dag_engine server \
|
||||
--port 8090 \
|
||||
--dags-dir /home/lucas/fn_registry/apps/dag_engine/dags_migrated \
|
||||
--db /home/lucas/fn_registry/apps/dag_engine/dag_engine.db \
|
||||
--scheduler
|
||||
```
|
||||
|
||||
Y reload + restart:
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart dag_engine.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Anadir un DAG nuevo (workflow)
|
||||
|
||||
### Paso a paso
|
||||
|
||||
1. **Crear YAML** en `apps/dag_engine/dags_migrated/<nombre>.yaml` (ver formato en seccion 3).
|
||||
2. **Validar** sin ejecutar:
|
||||
```bash
|
||||
./apps/dag_engine/dag_engine validate apps/dag_engine/dags_migrated/<nombre>.yaml
|
||||
```
|
||||
Salida esperada: `Validation: PASS`. Si falla, ver seccion 5 (diagnostico).
|
||||
3. **Probar ejecucion manual** una vez:
|
||||
```bash
|
||||
./apps/dag_engine/dag_engine run apps/dag_engine/dags_migrated/<nombre>.yaml
|
||||
```
|
||||
4. **Recargar scheduler** (toma el YAML automaticamente al iterar el dir):
|
||||
```bash
|
||||
systemctl --user restart dag_engine.service
|
||||
journalctl --user-unit dag_engine.service -n 30 --no-pager
|
||||
```
|
||||
Busca la linea `[scheduler] ticker started for <nombre> (<cron>)` en los logs.
|
||||
5. **Verificar en frontend**:
|
||||
- C++ ImGui: panel `DAGs` muestra el nuevo DAG. Pulsa `Refresh` si no aparece.
|
||||
- Web: `http://localhost:8090`.
|
||||
|
||||
### Disparo manual desde curl o frontend
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8090/api/dags/<nombre>/run
|
||||
```
|
||||
|
||||
Devuelve `{"dag":"<nombre>","run_id":"...","status":"accepted"}` y dispara el WS broadcast — los frontends ven la run en `<1s`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Formato YAML
|
||||
|
||||
Formato YAML propio de dag_engine. Schema: `name`, `description`, `schedule`, `env`, `tags`, `working_dir`, `steps[]`, `handlers` (alias `handler_on`).
|
||||
|
||||
### Ejemplo completo
|
||||
|
||||
```yaml
|
||||
name: my_pipeline
|
||||
description: "Pipeline diario que importa CSV y actualiza Metabase."
|
||||
group: finanzas # opcional, agrupa DAGs en listados
|
||||
type: graph # opcional: graph (default) | chain
|
||||
tags: [daily, csv, metabase] # opcional, filtros en la UI
|
||||
|
||||
# Variables de entorno (heredadas por todos los steps).
|
||||
env:
|
||||
- DATA_DIR: /home/lucas/data
|
||||
- SLACK_HOOK: ${SLACK_HOOK_PROD} # interpolacion de ENV del host
|
||||
|
||||
# Cron schedule. Puede ser string o lista.
|
||||
schedule:
|
||||
- "0 9 * * *" # 09:00 todos los dias
|
||||
- "0 21 * * 5" # 21:00 viernes (segundo trigger)
|
||||
|
||||
# Working dir + shell por defecto para todos los steps.
|
||||
working_dir: /home/lucas/fn_registry
|
||||
shell: /bin/bash
|
||||
timeout_sec: 1800 # 30 min para todo el DAG
|
||||
|
||||
steps:
|
||||
- name: ingest
|
||||
description: "Descarga CSV."
|
||||
command: ./bash/functions/pipelines/ingest_csv.sh
|
||||
timeout_sec: 300 # 5 min para este step
|
||||
env:
|
||||
- SOURCE_URL: https://example.com/data.csv
|
||||
|
||||
- name: transform
|
||||
description: "Limpieza y agregacion."
|
||||
script: |
|
||||
#!/usr/bin/env python3
|
||||
import pandas as pd
|
||||
df = pd.read_csv("$DATA_DIR/raw.csv")
|
||||
df.to_parquet("$DATA_DIR/clean.parquet")
|
||||
depends: [ingest] # debe terminar OK antes
|
||||
retry_policy:
|
||||
limit: 2 # reintentos en caso de fallo
|
||||
interval_sec: 60
|
||||
|
||||
- name: load_metabase
|
||||
command: ./bash/functions/metabase/refresh_dashboard.sh
|
||||
depends: [transform]
|
||||
continue_on:
|
||||
failure: true # no aborta el DAG aunque falle
|
||||
|
||||
- name: notify
|
||||
command: ./bash/functions/io/slack_send.sh "pipeline OK"
|
||||
depends: [load_metabase]
|
||||
|
||||
# Hooks de ciclo de vida.
|
||||
handler_on:
|
||||
success: ./bash/functions/io/notify_success.sh
|
||||
failure: ./bash/functions/io/notify_failure.sh
|
||||
exit: ./bash/functions/io/cleanup.sh
|
||||
```
|
||||
|
||||
### Campos del DAG (top-level)
|
||||
|
||||
| Campo | Tipo | Default | Que |
|
||||
|---|---|---|---|
|
||||
| `name` | string | (obligatorio) | Identificador unico. Debe matchear el filename sin extension. |
|
||||
| `description` | string | "" | Texto libre, aparece en la UI. |
|
||||
| `group` | string | "" | Agrupa DAGs en listados. |
|
||||
| `type` | string | `""` (graph) | `graph` o `chain`. graph = grafo dirigido por `depends`. chain = ejecucion secuencial implicita. |
|
||||
| `working_dir` | string | cwd del server | Path absoluto desde donde lanzar los steps. |
|
||||
| `shell` | string | `/bin/sh` | Shell para `command:`. |
|
||||
| `env` | list/map | [] | Variables de entorno DAG-wide. |
|
||||
| `schedule` | string/list | "" | Cron expressions (5 campos: min hour dom mon dow). Vacio = solo manual. |
|
||||
| `steps` | list | (obligatorio) | Pasos del DAG (>=1). |
|
||||
| `handler_on` | map | null | Hooks `init/success/failure/exit`. Alias: `handlers`. |
|
||||
| `tags` | list[string] | [] | Filtros en la UI. |
|
||||
| `timeout_sec` | int | 0 (sin timeout) | Timeout global del DAG en segundos. |
|
||||
|
||||
### Campos de cada step
|
||||
|
||||
| Campo | Tipo | Default | Que |
|
||||
|---|---|---|---|
|
||||
| `name` | string | (obligatorio) | Identificador del step dentro del DAG. |
|
||||
| `id` | string | "" | Override del id auto-generado. |
|
||||
| `description` | string | "" | Texto libre. |
|
||||
| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). |
|
||||
| `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. |
|
||||
| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run <id> <args...>` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. |
|
||||
| `args` | list[string] | [] | Args extra para `command` o para la `function`. |
|
||||
| `shell` | string | hereda | Override del shell. |
|
||||
| `dir` / `working_dir` | string | hereda | Working dir para este step. |
|
||||
| `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. |
|
||||
| `env` | list/map | hereda | Env del step (sobrescribe el del DAG). |
|
||||
| `continue_on.failure` | bool | false | Si true, el DAG sigue aunque este step falle. |
|
||||
| `continue_on.skipped` | bool | false | Si true, dependientes corren aunque este step quede skipped. |
|
||||
| `retry_policy.limit` | int | 0 | Reintentos. |
|
||||
| `retry_policy.interval_sec` | int | 0 | Segundos entre reintentos. |
|
||||
| `timeout_sec` | int | 0 (sin timeout) | Timeout del step. |
|
||||
| `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). |
|
||||
| `tags` | list[string] | [] | Tags por step (UI). |
|
||||
|
||||
### Function steps (coherencia con el registry)
|
||||
|
||||
Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: audit_capabilities
|
||||
function: audit_capability_groups_go_infra
|
||||
args: ["--json"]
|
||||
description: "Audita drift entre tags de capability group y paginas madre"
|
||||
```
|
||||
|
||||
Ventajas vs `command: ./fn run ...`:
|
||||
|
||||
- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable).
|
||||
- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente).
|
||||
- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step.
|
||||
- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error.
|
||||
- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`.
|
||||
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
|
||||
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
|
||||
|
||||
Ejemplo completo: `apps/dag_engine/dags_migrated/daily-registry-audit.yaml`.
|
||||
|
||||
### Cron schedule
|
||||
|
||||
5 campos clasicos: `min hour dom mon dow`. Ejemplos:
|
||||
|
||||
| Expresion | Significado |
|
||||
|---|---|
|
||||
| `0 9 * * *` | Todos los dias a las 09:00 |
|
||||
| `*/15 * * * *` | Cada 15 minutos |
|
||||
| `0 */6 * * *` | Cada 6 horas en punto |
|
||||
| `0 9 * * 1-5` | 09:00 lunes-viernes |
|
||||
| `0 21 * * 5` | 21:00 viernes |
|
||||
|
||||
Multiples cron en `schedule:` -> el DAG dispara por cada uno.
|
||||
|
||||
---
|
||||
|
||||
## 4. Comandos CLI
|
||||
|
||||
```bash
|
||||
./dag_engine run <path.yaml> # ejecuta un DAG ad-hoc
|
||||
./dag_engine list [dir] # lista DAGs con schedule + ultimo status
|
||||
./dag_engine status [dag_name] # historial de ejecuciones
|
||||
./dag_engine validate <path.yaml> # parse + validate (no ejecuta)
|
||||
./dag_engine server # arranca HTTP + WS hub + frontend embebido
|
||||
```
|
||||
|
||||
Flags del `server`:
|
||||
|
||||
| Flag | Default | Que |
|
||||
|---|---|---|
|
||||
| `--port` | 8090 | Puerto HTTP. |
|
||||
| `--dags-dir` | `apps/dag_engine/dags_migrated` (via systemd unit) | Dir scaneado para YAMLs. |
|
||||
| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. |
|
||||
| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Que hacer si algo falla
|
||||
|
||||
### 5.1. El DAG no aparece en la UI
|
||||
|
||||
**Sintoma:** anadiste un YAML pero `GET /api/dags` no lo lista.
|
||||
|
||||
| Causa | Diagnostico | Fix |
|
||||
|---|---|---|
|
||||
| YAML invalido | `./dag_engine validate <path>` muestra el error. | Corregir segun el mensaje (campo desconocido, indentacion, type wrong). |
|
||||
| Filename con extension fuera de `.yaml`/`.yml` | `ls apps/dag_engine/dags_migrated/` | Renombrar a `.yaml`. |
|
||||
| El servidor apunta a otro dir | `systemctl --user cat dag_engine.service` -> ver `--dags-dir`. | Ajustar unit y `daemon-reload + restart`. |
|
||||
| Cache UI antiguo | C++: pulsa `Refresh`. Web: `Ctrl+F5`. | — |
|
||||
|
||||
### 5.2. Validation: FAIL
|
||||
|
||||
`validate` muestra `parse error: ...` o `Validation: FAIL`. Causas tipicas:
|
||||
|
||||
| Mensaje | Causa | Fix |
|
||||
|---|---|---|
|
||||
| `yaml unmarshal: ...` | Sintaxis YAML rota (indentacion, tab vs espacios). | Usar 2 espacios consistentes. Validar online con `yamllint`. |
|
||||
| `dag_parse: step[N]: name is required` | Step sin `name:`. | Anadir `name:`. |
|
||||
| `dag_parse: step[N]: command or script required` | Step sin `command` ni `script`. | Anadir uno de los dos. |
|
||||
| `cycle detected: A -> B -> A` | `depends` forma ciclo. | Romper la dependencia o convertir uno de los nodos en step distinto. |
|
||||
| `unknown depends: <step>` | `depends:` referencia un step inexistente. | Comprobar nombres exactos (case-sensitive). |
|
||||
| `invalid cron: <expr>` | Cron mal formado (4 o 6 campos en vez de 5). | Verificar `0 9 * * *` (5 campos). |
|
||||
|
||||
### 5.3. El DAG corre pero un step falla
|
||||
|
||||
**Sintoma:** `status: failed` en la UI.
|
||||
|
||||
1. Abre `DAG Detail` y haz doble-click en el run rojo -> `Run Detail`.
|
||||
2. Expande el step que fallo (CollapsingHeader). Muestra `stdout` + `stderr`.
|
||||
3. Errores tipicos:
|
||||
|
||||
| stderr | Causa | Fix |
|
||||
|---|---|---|
|
||||
| `command not found` | `command:` apunta a un binario fuera de `PATH`. | Path absoluto o setear `env: [PATH: ...]`. |
|
||||
| `permission denied` | Script sin `chmod +x`. | `chmod +x <script>` (o usar `bash <script>`). |
|
||||
| `no such file or directory` | `working_dir:` mal o ruta relativa rota. | Path absoluto en `working_dir:`. |
|
||||
| Timeout | Step duro mas que `timeout_sec`. | Subir el limite o partir el step. |
|
||||
| Exit 137 / OOM kill | Out-of-memory. | Reducir batch o anadir swap. |
|
||||
|
||||
### 5.4. El scheduler no dispara
|
||||
|
||||
**Sintoma:** Hay `schedule:` valido pero el DAG no corre solo.
|
||||
|
||||
1. Verifica que el server arranco con `--scheduler`:
|
||||
```bash
|
||||
systemctl --user cat dag_engine.service | grep scheduler
|
||||
```
|
||||
2. Logs:
|
||||
```bash
|
||||
journalctl --user-unit dag_engine.service -n 50 --no-pager | grep -E "scheduler|ticker"
|
||||
```
|
||||
Debes ver `[scheduler] ticker started for <name> (<cron>), next: <ISO8601>`.
|
||||
3. Si `next:` es muy lejano (ej. en una semana) y necesitas probar -> dispara manual:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8090/api/dags/<name>/run
|
||||
```
|
||||
4. Hora del sistema descalibrada:
|
||||
```bash
|
||||
timedatectl status
|
||||
```
|
||||
Si difiere de la hora real, `sudo timedatectl set-ntp true`.
|
||||
|
||||
### 5.5. El frontend C++ no conecta WS
|
||||
|
||||
**Sintoma:** Panel `Live (WS)` muestra `disconnected`.
|
||||
|
||||
| Causa | Fix |
|
||||
|---|---|
|
||||
| Servidor caido | `systemctl --user status dag_engine.service`, `restart` si `inactive`. |
|
||||
| Puerto cambiado | El cliente apunta a `127.0.0.1:8090` por codigo (constante `g_ws_port`). Reedificar si cambiaste el puerto del server. |
|
||||
| Firewall Windows -> WSL | WSL2 expone `localhost`, normalmente OK. Si falla: `wsl --shutdown` y reabrir. |
|
||||
|
||||
### 5.6. Cleanup de runs viejos
|
||||
|
||||
`dag_runs` y `dag_step_results` crecen sin limite. Para limpiar:
|
||||
|
||||
```bash
|
||||
sqlite3 apps/dag_engine/dag_engine.db <<'SQL'
|
||||
DELETE FROM dag_step_results WHERE run_id IN (
|
||||
SELECT id FROM dag_runs WHERE started_at < datetime('now', '-30 days')
|
||||
);
|
||||
DELETE FROM dag_runs WHERE started_at < datetime('now', '-30 days');
|
||||
VACUUM;
|
||||
SQL
|
||||
```
|
||||
|
||||
### 5.7. Restaurar desde backup
|
||||
|
||||
Si rompes `dags_migrated/`, recupera desde el snapshot de `backup_all_bash_pipelines` (BACKUP_ROOT por defecto `~/backups/fn_registry`):
|
||||
|
||||
```bash
|
||||
cp ~/backups/fn_registry/registry/daily.0/dags_migrated/*.yaml \
|
||||
apps/dag_engine/dags_migrated/ 2>/dev/null || \
|
||||
git checkout HEAD -- apps/dag_engine/dags_migrated/
|
||||
systemctl --user restart dag_engine.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Endpoints HTTP
|
||||
|
||||
| Metodo | Path | Que |
|
||||
|---|---|---|
|
||||
| GET | `/api/dags` | Lista DAGs + last_run + last_runs[5]. |
|
||||
| GET | `/api/dags/{name}` | Detalle + validation. |
|
||||
| POST | `/api/dags/{name}/run` | Dispara ejecucion (trigger=`api`). Devuelve `run_id`. |
|
||||
| GET | `/api/runs` | Historial. Query: `dag`, `limit`, `offset`. |
|
||||
| GET | `/api/runs/{id}` | Detalle de un run + sus step_results. |
|
||||
| GET | `/api/ws/dagruns` | WebSocket. Snapshot + deltas en vivo (issue 0095). |
|
||||
| GET | `/api/scheduler/status` | Tickers activos. |
|
||||
| POST | `/api/scheduler/start` | Arranca scheduler (si no estaba). |
|
||||
| POST | `/api/scheduler/stop` | Para scheduler. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Referencias
|
||||
|
||||
- Schema parser: `functions/core/dag_parse.go` (frontmatter en `dag_parse_go_core`).
|
||||
- Validator: `functions/core/dag_validate.go` (`dag_validate_go_core`).
|
||||
- Topo sort: `functions/core/dag_topo_sort.go` (`dag_topo_sort_go_core`).
|
||||
- Cron: `functions/core/parse_cron_expr.go` + `next_cron_time.go`.
|
||||
- Frontend C++: `cpp/apps/dag_engine_ui/` (issue 0095).
|
||||
- WS hub: `apps/dag_engine/events.go`.
|
||||
- dag_engine es el scheduler oficial del ecosistema. Single-binary Go + SQLite, sin dependencias externas.
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// RegisterAPI sets up all HTTP routes on the given mux.
|
||||
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, frontendFS fs.FS) {
|
||||
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, hub *DagRunHub, frontendFS fs.FS) {
|
||||
// API routes.
|
||||
mux.HandleFunc("GET /api/dags", handleListDags(executor))
|
||||
mux.HandleFunc("GET /api/dags/{name}", handleGetDag(executor))
|
||||
@@ -15,10 +15,18 @@ func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, f
|
||||
mux.HandleFunc("GET /api/runs", handleListRuns(executor))
|
||||
mux.HandleFunc("GET /api/runs/{id}", handleGetRun(executor))
|
||||
|
||||
// Function lookup proxy a registry.db (read-only).
|
||||
mux.HandleFunc("GET /api/functions/{id}", handleGetFunction())
|
||||
|
||||
mux.HandleFunc("POST /api/scheduler/start", handleSchedulerStart(scheduler))
|
||||
mux.HandleFunc("POST /api/scheduler/stop", handleSchedulerStop(scheduler))
|
||||
mux.HandleFunc("GET /api/scheduler/status", handleSchedulerStatus(scheduler))
|
||||
|
||||
// Live updates (WS hub).
|
||||
if hub != nil {
|
||||
mux.HandleFunc("GET /api/ws/dagruns", handleDagRunsWS(hub))
|
||||
}
|
||||
|
||||
// Frontend SPA fallback.
|
||||
if frontendFS != nil {
|
||||
mux.Handle("/", spaHandler(frontendFS))
|
||||
|
||||
+65
-7
@@ -2,7 +2,8 @@
|
||||
name: dag_engine
|
||||
lang: go
|
||||
domain: infra
|
||||
description: "Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite."
|
||||
version: 0.1.0
|
||||
description: "Motor de ejecucion de DAGs del fn_registry: CLI + servidor HTTP + scheduler cron. Schema YAML propio con `function:` para invocar funciones del registry (`fn run <id>`) y `command:` para shell. Historial en SQLite. Scheduler oficial del ecosistema."
|
||||
tags: [service, dag, workflow, scheduler, web, cron]
|
||||
uses_functions:
|
||||
- dag_parse_go_core
|
||||
@@ -27,6 +28,18 @@ uses_types:
|
||||
framework: "net/http + vite + react"
|
||||
entry_point: "main.go"
|
||||
dir_path: "apps/dag_engine"
|
||||
service:
|
||||
port: 8090
|
||||
health_endpoint: /api/dags
|
||||
health_timeout_s: 3
|
||||
systemd_unit: dag_engine.service
|
||||
systemd_scope: user
|
||||
restart_policy: always
|
||||
runtime: systemd-user
|
||||
pc_targets:
|
||||
- aurgi-pc
|
||||
- home-wsl
|
||||
is_local_only: false
|
||||
---
|
||||
|
||||
## Arquitectura
|
||||
@@ -71,15 +84,60 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
|
||||
|
||||
```bash
|
||||
# CLI
|
||||
./dag-engine run ~/dagu/dags/example.yaml
|
||||
./dag-engine list ~/dagu/dags/
|
||||
./dag-engine run apps/dag_engine/dags_migrated/fn_backup.yaml
|
||||
./dag-engine list apps/dag_engine/dags_migrated/
|
||||
|
||||
# Servidor web
|
||||
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
|
||||
# Servidor web (production: gestionado por dag_engine.service systemd user unit)
|
||||
./dag-engine server --port 8090 --dags-dir apps/dag_engine/dags_migrated/ --scheduler
|
||||
# Browser: http://localhost:8090
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
|
||||
Puerto por defecto 8090 (mismo que Dagu).
|
||||
Schema YAML propio (ver `README.md` seccion 3 + ejemplos en `dags_migrated/`). Steps tipo `function:` invocan `fn run <id>` y propagan `function_id` a `dag_step_results` para el bucle reactivo. Puerto default 8090.
|
||||
|
||||
### 2026-05-16 — Fix function-not-found en steps `function:` + panel Logs en RunDetail `[done]`
|
||||
|
||||
Sintoma: `fn_backup` y `daily-registry-audit` fallaron 3 noches seguidas con `error: function "<id>" not found (tried as ID and name)` aunque las funciones existen en `registry.db` raiz.
|
||||
|
||||
Raiz: servicio systemd `dag_engine.service` tiene `WorkingDirectory=/home/lucas/fn_registry/apps/dag_engine`. Binario `fn` resuelve `registry.db` por (1) `FN_REGISTRY_ROOT`, (2) `root()` walk-up buscando `go.mod`, (3) exe dir (`cmd/fn/ops.go:1597-1628`). Sin `FN_REGISTRY_ROOT` seteado, (2) encuentra el `go.mod` de `apps/dag_engine/` y devuelve ese dir — donde habia una copia stale `apps/dag_engine/registry.db` (262 KB, May 15) sin las funciones recien creadas. Viola regla `.claude/rules/db_locations.md` (registry.db SOLO en raiz).
|
||||
|
||||
Fix:
|
||||
- Borrado `apps/dag_engine/registry.db` stale.
|
||||
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT=/home/lucas/fn_registry`, `FN_BIN=/home/lucas/fn_registry/fn`, `PATH=/usr/local/go/bin:/home/lucas/go/bin:...`, `HOME=/home/lucas`. Sin PATH el step `go vet` fallaba con `exec: "go": executable file not found in $PATH`.
|
||||
- `apps/dag_engine/executor.go`: para steps `function:` el spawn exporta `FN_REGISTRY_ROOT=<root>` en env y, si `step.dir`/`working_dir` vacios, fija `dir = fnRegistryRoot`. Belt-and-suspenders: aunque alguien lance el binario sin systemd, los `function:` steps usan el root canonico.
|
||||
|
||||
Verificacion: `POST /api/dags/daily-registry-audit/run` -> step `audit_capabilities` pasa (387 ms) en vez de fallar con not-found. Restantes failures (`audit_artefacts` exit 1, `fn_backup` exit 4 sin respetar `continue_on.exit_code`) son bugs reales independientes — fuera de scope.
|
||||
|
||||
### 2026-05-16 — Panel Logs en RunDetail (frontend) `[done]`
|
||||
|
||||
- `apps/dag_engine/frontend/src/pages/RunDetail.tsx`: nuevo `<Paper>` "Logs" al final con `<Code block>` scrollable (max-h 480) + `CopyButton` de Mantine (icono toggle copy/check teal).
|
||||
- Helper `buildLogText(run, steps)` compone texto plano: metadata del run (dag, path, status, trigger, started/finished ISO, duration ms, error) + por step (`[status] name exit=N Nms`, started, finished, error, stdout, stderr indentado 4 espacios).
|
||||
- Permite pegar log entero al LLM para debugging sin abrir N collapses del `StepTimeline`.
|
||||
- Build frontend pendiente: `pnpm build` rompe por errores preexistentes (`StepTimeline.tsx:49` usa API legacy `<Collapse in={opened}>`; `main.tsx:1` importa `@mantine/core/styles.css` sin tipos). Edit de RunDetail type-checkea limpio.
|
||||
|
||||
### 2026-05-16 — BBDDs canonicas (referencia rapida)
|
||||
|
||||
- `dag_engine.db`: `apps/dag_engine/dag_engine.db` (+ WAL sidecars). Migrations en `apps/dag_engine/store/migrations/` (`001_init.sql`, `002_step_function_id.sql`). Tablas `dag_runs`, `dag_step_results`.
|
||||
- NO debe coexistir copia de `registry.db` en este dir (viola `db_locations.md`). Si reaparece: borrarla.
|
||||
|
||||
## Lo siguiente que pega
|
||||
|
||||
- `audit_artefacts` falla con exit 1 en `daily-registry-audit` — investigar stderr real (probablemente artefacto huerfano o git drift). Step independiente, no bloquea el resto del DAG.
|
||||
- `fn_backup` step `run_backup_all` sale con exit 4 y el DAG no respeta `continue_on.exit_code: [4]`. Bug en executor: parsear `step.ContinueOn.ExitCode []int` y comparar con `result.ExitCode`. Hoy solo se mira `step.ContinueOn.Failure` (bool).
|
||||
- Frontend `pnpm build` roto por API drift de Mantine en `StepTimeline.tsx` (`<Collapse in={opened}>`) y CSS type import en `main.tsx`. Fix junto con un refresh general de tipos.
|
||||
|
||||
## Documentacion de usuario
|
||||
|
||||
Guia completa (formato YAML, anadir DAGs, troubleshooting, endpoints HTTP):
|
||||
**[apps/dag_engine/README.md](README.md)**.
|
||||
|
||||
|
||||
## Capability growth log
|
||||
|
||||
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
||||
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
|
||||
- `patch`: bugfix sin cambio observable.
|
||||
|
||||
- v0.1.0 (2026-05-18) — baseline.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: fn_backup
|
||||
description: Backup diario de fn_registry (registry.db + operations.db + vaults) via funcion del registry
|
||||
|
||||
schedule:
|
||||
- "0 3 * * *"
|
||||
|
||||
tags: [backup, registry, daily]
|
||||
|
||||
env:
|
||||
- BACKUP_ROOT: /home/lucas/backups/fn_registry
|
||||
|
||||
steps:
|
||||
- name: ensure_dirs
|
||||
command: mkdir -p ${BACKUP_ROOT}
|
||||
|
||||
- name: run_backup_all
|
||||
description: "Snapshot atomico de registry.db + operations.db + vaults con retention 7/4/12"
|
||||
function: backup_all_bash_pipelines
|
||||
args: ["${BACKUP_ROOT}"]
|
||||
continue_on:
|
||||
exit_code: [4]
|
||||
depends: [ensure_dirs]
|
||||
|
||||
- name: report_status
|
||||
command: bash -c 'ls -lh ${BACKUP_ROOT}/registry/daily.0 ${BACKUP_ROOT}/operations/*/daily.0 2>/dev/null | tail -20'
|
||||
depends: [run_backup_all]
|
||||
@@ -0,0 +1,51 @@
|
||||
name: revision-viernes-finanzas
|
||||
description: Revisión semanal de finanzas personales - ingesta, informe y push a Gitea
|
||||
tags: [finanzas, semanal]
|
||||
type: graph
|
||||
|
||||
schedule: "0 9 * * 5"
|
||||
|
||||
env:
|
||||
- PROJECT_DIR: /home/lucas/analysis/finanzas_personales
|
||||
- PYTHON: /home/lucas/analysis/finanzas_personales/.venv/bin/python
|
||||
|
||||
handler_on:
|
||||
failure:
|
||||
command: echo "[$(date)] FALLÓ revision-viernes-finanzas" >> /home/lucas/dagu/logs/failures.log
|
||||
|
||||
steps:
|
||||
- id: ingest
|
||||
description: Procesar archivos nuevos del inbox (BBVA xlsx + Revolut csv)
|
||||
working_dir: ${PROJECT_DIR}
|
||||
command: ./bin/ingest -skip-notebooks
|
||||
continue_on:
|
||||
failure: true
|
||||
|
||||
- id: informe
|
||||
description: Generar informe semanal de cumplimiento del presupuesto
|
||||
command: ${PYTHON} /home/lucas/dagu/scripts/informe_finanzas.py
|
||||
depends: [ingest]
|
||||
|
||||
- id: git_push
|
||||
description: Commit y push del informe a Gitea
|
||||
working_dir: ${PROJECT_DIR}
|
||||
script: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if git diff --quiet data/04_output/informe_semanal.md 2>/dev/null && \
|
||||
! git ls-files --others --exclude-standard | grep -q informe_semanal.md; then
|
||||
echo "Sin cambios en el informe, skip push"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add data/04_output/informe_semanal.md
|
||||
git add data/03_processed/ 2>/dev/null || true
|
||||
|
||||
git commit -m "Informe semanal $(date +%Y-%m-%d)
|
||||
|
||||
Co-Authored-By: Dagu Automation <noreply@dagu.dev>"
|
||||
|
||||
git push origin master:main
|
||||
echo "Push completado"
|
||||
depends: [informe]
|
||||
@@ -0,0 +1,438 @@
|
||||
package main
|
||||
|
||||
// WebSocket hub para live updates de dag_runs + dag_step_results.
|
||||
// Patron: sqlite_api/events.go (CallMonitorHub) — issue 0095.
|
||||
//
|
||||
// Diseño:
|
||||
// - Hub global con N subscribers WS.
|
||||
// - Ticker arranca solo con >=1 subscriber. Cero overhead si nadie mira.
|
||||
// - Cada tick (500ms): query rowid>watermark + activos (status running/pending)
|
||||
// + recientes finished (ultimos 5s) -> broadcast upsert.
|
||||
// - Snapshot inicial: lista de DAGs + ultimos 50 runs + step_results.
|
||||
// - El cliente trata `runs` y `steps` como upserts por id.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
|
||||
"dag-engine/store"
|
||||
)
|
||||
|
||||
const (
|
||||
dagWSTickInterval = 500 * time.Millisecond
|
||||
dagWSTickIntervalIdle = 2 * time.Second
|
||||
dagWSIdleThreshold = 30 * time.Second
|
||||
dagWSSnapshotRuns = 50
|
||||
dagWSBroadcastTimeout = 2 * time.Second
|
||||
dagWSRecentFinishedS = 5
|
||||
)
|
||||
|
||||
type wsRun struct {
|
||||
ID string `json:"id"`
|
||||
DagName string `json:"dag_name"`
|
||||
DagPath string `json:"dag_path"`
|
||||
Status string `json:"status"`
|
||||
Trigger string `json:"trigger"`
|
||||
StartedAt string `json:"started_at"`
|
||||
FinishedAt string `json:"finished_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type wsStep struct {
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepName string `json:"step_name"`
|
||||
Status string `json:"status"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Stdout string `json:"stdout,omitempty"`
|
||||
Stderr string `json:"stderr,omitempty"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
FinishedAt string `json:"finished_at,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type wsWatermark struct {
|
||||
Runs int64 `json:"runs"`
|
||||
Steps int64 `json:"steps"`
|
||||
}
|
||||
|
||||
type wsDagMessage struct {
|
||||
Type string `json:"type"` // snapshot|delta|ping
|
||||
Watermark wsWatermark `json:"watermark"`
|
||||
Dags []DagInfo `json:"dags,omitempty"`
|
||||
Runs []wsRun `json:"runs,omitempty"`
|
||||
Steps []wsStep `json:"steps,omitempty"`
|
||||
ServerTime int64 `json:"server_time"`
|
||||
}
|
||||
|
||||
type wsDagClientCmd struct {
|
||||
Watermark wsWatermark `json:"watermark,omitempty"`
|
||||
}
|
||||
|
||||
type dagSubscriber struct {
|
||||
conn *websocket.Conn
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
out chan wsDagMessage
|
||||
watermark wsWatermark
|
||||
}
|
||||
|
||||
// DagRunHub broadcastea cambios de dag_runs + dag_step_results a clientes WS.
|
||||
type DagRunHub struct {
|
||||
db *store.DB
|
||||
executor *Executor
|
||||
|
||||
mu sync.Mutex
|
||||
subscribers map[*dagSubscriber]struct{}
|
||||
tickerStop chan struct{}
|
||||
tickerOn bool
|
||||
watermark wsWatermark
|
||||
lastEventAt time.Time
|
||||
}
|
||||
|
||||
func NewDagRunHub(db *store.DB, executor *Executor) *DagRunHub {
|
||||
return &DagRunHub{
|
||||
db: db,
|
||||
executor: executor,
|
||||
subscribers: make(map[*dagSubscriber]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DagRunHub) register(s *dagSubscriber) {
|
||||
h.mu.Lock()
|
||||
h.subscribers[s] = struct{}{}
|
||||
shouldStart := !h.tickerOn
|
||||
if shouldStart {
|
||||
h.tickerStop = make(chan struct{})
|
||||
h.tickerOn = true
|
||||
h.lastEventAt = time.Now()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
if shouldStart {
|
||||
go h.tickerLoop()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DagRunHub) unregister(s *dagSubscriber) {
|
||||
h.mu.Lock()
|
||||
if _, ok := h.subscribers[s]; !ok {
|
||||
h.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(h.subscribers, s)
|
||||
close(s.out)
|
||||
shouldStop := h.tickerOn && len(h.subscribers) == 0
|
||||
if shouldStop {
|
||||
close(h.tickerStop)
|
||||
h.tickerOn = false
|
||||
}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) tickerLoop() {
|
||||
interval := dagWSTickInterval
|
||||
t := time.NewTimer(interval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-h.tickerStop:
|
||||
return
|
||||
case <-t.C:
|
||||
runs, steps, wm, err := h.fetchDelta(h.getWatermark())
|
||||
if err != nil {
|
||||
log.Printf("[dagws] fetchDelta: %v", err)
|
||||
} else if len(runs) > 0 || len(steps) > 0 {
|
||||
h.setWatermark(wm)
|
||||
h.recordActivity()
|
||||
h.broadcast(wsDagMessage{
|
||||
Type: "delta",
|
||||
Watermark: wm,
|
||||
Runs: runs,
|
||||
Steps: steps,
|
||||
ServerTime: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if time.Since(h.lastActivityAt()) > dagWSIdleThreshold {
|
||||
interval = dagWSTickIntervalIdle
|
||||
} else {
|
||||
interval = dagWSTickInterval
|
||||
}
|
||||
t.Reset(interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DagRunHub) getWatermark() wsWatermark {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return h.watermark
|
||||
}
|
||||
|
||||
func (h *DagRunHub) setWatermark(v wsWatermark) {
|
||||
h.mu.Lock()
|
||||
if v.Runs > h.watermark.Runs {
|
||||
h.watermark.Runs = v.Runs
|
||||
}
|
||||
if v.Steps > h.watermark.Steps {
|
||||
h.watermark.Steps = v.Steps
|
||||
}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) recordActivity() {
|
||||
h.mu.Lock()
|
||||
h.lastEventAt = time.Now()
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) lastActivityAt() time.Time {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return h.lastEventAt
|
||||
}
|
||||
|
||||
// fetchDelta devuelve runs/steps con (rowid > watermark) OR (status in-flight)
|
||||
// OR (recently finished). Watermark devuelto = max rowid visto.
|
||||
func (h *DagRunHub) fetchDelta(since wsWatermark) ([]wsRun, []wsStep, wsWatermark, error) {
|
||||
conn := h.db.Conn()
|
||||
if conn == nil {
|
||||
return nil, nil, since, nil
|
||||
}
|
||||
cutoff := time.Now().Add(-time.Duration(dagWSRecentFinishedS) * time.Second).Format(time.RFC3339)
|
||||
|
||||
runs, maxRuns, err := scanRuns(conn, `
|
||||
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
|
||||
COALESCE(finished_at,''), error
|
||||
FROM dag_runs
|
||||
WHERE rowid > ?
|
||||
OR status IN ('running','pending')
|
||||
OR (finished_at IS NOT NULL AND finished_at >= ?)
|
||||
ORDER BY rowid ASC`, since.Runs, cutoff)
|
||||
if err != nil {
|
||||
return nil, nil, since, err
|
||||
}
|
||||
|
||||
steps, maxSteps, err := scanSteps(conn, `
|
||||
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
|
||||
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
|
||||
FROM dag_step_results
|
||||
WHERE rowid > ?
|
||||
OR status IN ('running','pending')
|
||||
OR (finished_at IS NOT NULL AND finished_at >= ?)
|
||||
ORDER BY rowid ASC`, since.Steps, cutoff)
|
||||
if err != nil {
|
||||
return runs, nil, since, err
|
||||
}
|
||||
|
||||
out := wsWatermark{Runs: maxRuns, Steps: maxSteps}
|
||||
if out.Runs < since.Runs {
|
||||
out.Runs = since.Runs
|
||||
}
|
||||
if out.Steps < since.Steps {
|
||||
out.Steps = since.Steps
|
||||
}
|
||||
return runs, steps, out, nil
|
||||
}
|
||||
|
||||
// fetchSnapshot devuelve DAGs + ultimos N runs + sus step_results + watermark.
|
||||
func (h *DagRunHub) fetchSnapshot() ([]DagInfo, []wsRun, []wsStep, wsWatermark, error) {
|
||||
dags, err := h.executor.ListDAGs()
|
||||
if err != nil {
|
||||
log.Printf("[dagws] list dags: %v", err)
|
||||
dags = nil
|
||||
}
|
||||
conn := h.db.Conn()
|
||||
if conn == nil {
|
||||
return dags, nil, nil, wsWatermark{}, nil
|
||||
}
|
||||
|
||||
runs, maxRuns, err := scanRuns(conn, `
|
||||
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
|
||||
COALESCE(finished_at,''), error
|
||||
FROM dag_runs
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ?`, dagWSSnapshotRuns)
|
||||
if err != nil {
|
||||
return dags, nil, nil, wsWatermark{}, err
|
||||
}
|
||||
|
||||
steps, maxSteps, err := scanSteps(conn, `
|
||||
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
|
||||
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
|
||||
FROM dag_step_results
|
||||
WHERE run_id IN (SELECT id FROM dag_runs ORDER BY started_at DESC LIMIT ?)
|
||||
ORDER BY rowid ASC`, dagWSSnapshotRuns)
|
||||
if err != nil {
|
||||
return dags, runs, nil, wsWatermark{Runs: maxRuns}, err
|
||||
}
|
||||
|
||||
return dags, runs, steps, wsWatermark{Runs: maxRuns, Steps: maxSteps}, nil
|
||||
}
|
||||
|
||||
func scanRuns(conn *sql.DB, q string, args ...any) ([]wsRun, int64, error) {
|
||||
rows, err := conn.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []wsRun
|
||||
var max int64
|
||||
for rows.Next() {
|
||||
var r wsRun
|
||||
var rowid int64
|
||||
if err := rows.Scan(&rowid, &r.ID, &r.DagName, &r.DagPath, &r.Status,
|
||||
&r.Trigger, &r.StartedAt, &r.FinishedAt, &r.Error); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if rowid > max {
|
||||
max = rowid
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, max, rows.Err()
|
||||
}
|
||||
|
||||
func scanSteps(conn *sql.DB, q string, args ...any) ([]wsStep, int64, error) {
|
||||
rows, err := conn.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []wsStep
|
||||
var max int64
|
||||
for rows.Next() {
|
||||
var s wsStep
|
||||
var rowid int64
|
||||
if err := rows.Scan(&rowid, &s.ID, &s.RunID, &s.StepName, &s.Status,
|
||||
&s.ExitCode, &s.Stdout, &s.Stderr, &s.StartedAt, &s.FinishedAt,
|
||||
&s.DurationMs, &s.Error); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if rowid > max {
|
||||
max = rowid
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, max, rows.Err()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) broadcast(msg wsDagMessage) {
|
||||
h.mu.Lock()
|
||||
subs := make([]*dagSubscriber, 0, len(h.subscribers))
|
||||
for s := range h.subscribers {
|
||||
subs = append(subs, s)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
for _, s := range subs {
|
||||
select {
|
||||
case s.out <- msg:
|
||||
default:
|
||||
log.Printf("[dagws] dropping frame for slow subscriber")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDagRunsWS upgrade WS y gestiona lifecycle.
|
||||
// Endpoint: GET /api/ws/dagruns
|
||||
func handleDagRunsWS(hub *DagRunHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[dagws] accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close(websocket.StatusInternalError, "closing")
|
||||
|
||||
ctx, cancel := context.WithCancel(r.Context())
|
||||
defer cancel()
|
||||
|
||||
sub := &dagSubscriber{
|
||||
conn: conn,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
out: make(chan wsDagMessage, 64),
|
||||
}
|
||||
hub.register(sub)
|
||||
defer hub.unregister(sub)
|
||||
|
||||
dags, runs, steps, wm, err := hub.fetchSnapshot()
|
||||
if err != nil {
|
||||
log.Printf("[dagws] snapshot: %v", err)
|
||||
conn.Close(websocket.StatusInternalError, "snapshot failed")
|
||||
return
|
||||
}
|
||||
hub.setWatermark(wm)
|
||||
initial := wsDagMessage{
|
||||
Type: "snapshot",
|
||||
Watermark: wm,
|
||||
Dags: dags,
|
||||
Runs: runs,
|
||||
Steps: steps,
|
||||
ServerTime: time.Now().Unix(),
|
||||
}
|
||||
if err := wsjson.Write(ctx, conn, initial); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
readErr := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
var cmd wsDagClientCmd
|
||||
if err := wsjson.Read(ctx, conn, &cmd); err != nil {
|
||||
readErr <- err
|
||||
return
|
||||
}
|
||||
if cmd.Watermark.Runs > 0 || cmd.Watermark.Steps > 0 {
|
||||
runs, steps, wm, err := hub.fetchDelta(cmd.Watermark)
|
||||
if err == nil && (len(runs) > 0 || len(steps) > 0) {
|
||||
hub.setWatermark(wm)
|
||||
select {
|
||||
case sub.out <- wsDagMessage{
|
||||
Type: "delta",
|
||||
Watermark: wm,
|
||||
Runs: runs,
|
||||
Steps: steps,
|
||||
ServerTime: time.Now().Unix(),
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-readErr:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case msg, ok := <-sub.out:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
wctx, wcancel := context.WithTimeout(ctx, dagWSBroadcastTimeout)
|
||||
err := wsjson.Write(wctx, conn, msg)
|
||||
wcancel()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-20
@@ -156,22 +156,49 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
|
||||
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
|
||||
stepID := generateID()
|
||||
now := time.Now()
|
||||
|
||||
// Resolve command source: function (registry) takes precedence over command/script.
|
||||
var command string
|
||||
var stepFunctionID string
|
||||
var fnRegistryRoot string
|
||||
if step.Function != "" {
|
||||
stepFunctionID = step.Function
|
||||
fnRegistryRoot = os.Getenv("FN_REGISTRY_ROOT")
|
||||
if fnRegistryRoot == "" {
|
||||
fnRegistryRoot = "/home/lucas/fn_registry"
|
||||
}
|
||||
fnBin := os.Getenv("FN_BIN")
|
||||
if fnBin == "" {
|
||||
fnBin = fnRegistryRoot + "/fn"
|
||||
}
|
||||
parts := []string{fnBin, "run", step.Function}
|
||||
parts = append(parts, step.Args...)
|
||||
command = strings.Join(parts, " ")
|
||||
} else if step.Command != "" {
|
||||
command = step.Command
|
||||
} else if step.Script != "" {
|
||||
command = step.Script
|
||||
}
|
||||
|
||||
e.store.InsertStepResult(&store.DagStepResult{
|
||||
ID: stepID,
|
||||
RunID: runID,
|
||||
StepName: stepName(step),
|
||||
Status: "running",
|
||||
StartedAt: &now,
|
||||
ID: stepID,
|
||||
RunID: runID,
|
||||
StepName: stepName(step),
|
||||
FunctionID: stepFunctionID,
|
||||
Status: "running",
|
||||
StartedAt: &now,
|
||||
})
|
||||
|
||||
// Build environment.
|
||||
env := buildStepEnv(dag, step, daguEnvPath, outputs)
|
||||
|
||||
// Determine command.
|
||||
command := step.Command
|
||||
if command == "" && step.Script != "" {
|
||||
command = step.Script
|
||||
// For function: steps, force FN_REGISTRY_ROOT into env so `fn run`
|
||||
// resolves the canonical registry.db (not whatever lives at the spawn cwd).
|
||||
// Prevents the apps/dag_engine/registry.db stale-shadow bug (2026-05-16).
|
||||
if stepFunctionID != "" {
|
||||
env = append(env, "FN_REGISTRY_ROOT="+fnRegistryRoot)
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
|
||||
return nil
|
||||
@@ -182,11 +209,15 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
|
||||
command = resolveStepRefs(command, outputs)
|
||||
mu.Unlock()
|
||||
|
||||
// Determine working directory.
|
||||
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
|
||||
// so `fn` resolves registry.db correctly via go.mod walk-up.
|
||||
dir := step.Dir
|
||||
if dir == "" {
|
||||
dir = dag.WorkingDir
|
||||
}
|
||||
if dir == "" && stepFunctionID != "" {
|
||||
dir = fnRegistryRoot
|
||||
}
|
||||
|
||||
shell := step.Shell
|
||||
if shell == "" {
|
||||
@@ -326,14 +357,15 @@ func generateID() string {
|
||||
|
||||
// DagInfo summarizes a DAG file for listing.
|
||||
type DagInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Schedule []string `json:"schedule,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
FilePath string `json:"file_path"`
|
||||
Valid bool `json:"valid"`
|
||||
LastRun *store.DagRun `json:"last_run,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Schedule []string `json:"schedule,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
FilePath string `json:"file_path"`
|
||||
Valid bool `json:"valid"`
|
||||
LastRun *store.DagRun `json:"last_run,omitempty"`
|
||||
LastRuns []store.DagRun `json:"last_runs,omitempty"` // 5 mas recientes (mas reciente primero)
|
||||
}
|
||||
|
||||
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
|
||||
@@ -379,10 +411,11 @@ func (e *Executor) ListDAGs() ([]DagInfo, error) {
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
// Attach last run info.
|
||||
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
|
||||
// Attach last 5 runs (most recent first).
|
||||
runs, _, _ := e.store.ListRuns(dag.Name, 5, 0)
|
||||
if len(runs) > 0 {
|
||||
info.LastRun = &runs[0]
|
||||
info.LastRuns = runs
|
||||
}
|
||||
|
||||
dags = append(dags, info)
|
||||
|
||||
@@ -9,12 +9,63 @@ import {
|
||||
Paper,
|
||||
Alert,
|
||||
Loader,
|
||||
CopyButton,
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
Code,
|
||||
} from "@mantine/core";
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react";
|
||||
import { getRun } from "../api";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { StepTimeline } from "../components/StepTimeline";
|
||||
import type { RunDetail as RunDetailType } from "../types";
|
||||
import type { RunDetail as RunDetailType, DagStepResult, DagRun } from "../types";
|
||||
|
||||
function buildLogText(run: DagRun, steps: DagStepResult[]): string {
|
||||
const lines: string[] = [];
|
||||
const started = run.StartedAt ? new Date(run.StartedAt) : null;
|
||||
const finished = run.FinishedAt ? new Date(run.FinishedAt) : null;
|
||||
const durationMs =
|
||||
started && finished ? finished.getTime() - started.getTime() : null;
|
||||
|
||||
lines.push(`=== DAG run ${run.ID} ===`);
|
||||
lines.push(`dag: ${run.DagName}`);
|
||||
lines.push(`path: ${run.DagPath}`);
|
||||
lines.push(`status: ${run.Status}`);
|
||||
lines.push(`trigger: ${run.Trigger}`);
|
||||
lines.push(`started: ${started ? started.toISOString() : "-"}`);
|
||||
lines.push(`finished: ${finished ? finished.toISOString() : "-"}`);
|
||||
lines.push(
|
||||
`duration: ${durationMs !== null ? `${durationMs} ms` : "running..."}`
|
||||
);
|
||||
if (run.Error) {
|
||||
lines.push("");
|
||||
lines.push("run error:");
|
||||
lines.push(run.Error);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(`--- steps (${steps.length}) ---`);
|
||||
for (const s of steps) {
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`[${s.Status}] ${s.StepName} exit=${s.ExitCode} ${s.DurationMs}ms`
|
||||
);
|
||||
if (s.StartedAt) lines.push(` started: ${s.StartedAt}`);
|
||||
if (s.FinishedAt) lines.push(` finished: ${s.FinishedAt}`);
|
||||
if (s.Error) {
|
||||
lines.push(" error:");
|
||||
lines.push(s.Error.split("\n").map((l) => " " + l).join("\n"));
|
||||
}
|
||||
if (s.Stdout) {
|
||||
lines.push(" stdout:");
|
||||
lines.push(s.Stdout.split("\n").map((l) => " " + l).join("\n"));
|
||||
}
|
||||
if (s.Stderr) {
|
||||
lines.push(" stderr:");
|
||||
lines.push(s.Stderr.split("\n").map((l) => " " + l).join("\n"));
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function RunDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -100,6 +151,41 @@ export function RunDetail() {
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" withBorder>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Title order={4}>Logs</Title>
|
||||
<CopyButton value={buildLogText(run, steps || [])} timeout={1500}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={copied ? "Copiado" : "Copiar log completo"}
|
||||
withArrow
|
||||
position="left"
|
||||
>
|
||||
<ActionIcon
|
||||
variant={copied ? "filled" : "light"}
|
||||
color={copied ? "teal" : "blue"}
|
||||
onClick={copy}
|
||||
aria-label="Copiar logs"
|
||||
>
|
||||
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
<Code
|
||||
block
|
||||
style={{
|
||||
maxHeight: 480,
|
||||
overflow: "auto",
|
||||
whiteSpace: "pre",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{buildLogText(run, steps || [])}
|
||||
</Code>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
fn-registry v0.0.0-00010101000000-000000000000
|
||||
github.com/mattn/go-sqlite3 v1.14.37
|
||||
nhooyr.io/websocket v1.8.17
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -28,19 +29,21 @@ require (
|
||||
github.com/marcboeker/go-duckdb v1.8.5 // indirect
|
||||
github.com/paulmach/orb v0.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+18
-10
@@ -111,44 +111,50 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
|
||||
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -166,3 +172,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
|
||||
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
|
||||
@@ -30,10 +30,10 @@ func handleGetDag(executor *Executor) http.HandlerFunc {
|
||||
runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0)
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"info": info,
|
||||
"dag": dag,
|
||||
"validation": validation,
|
||||
"runs": runs,
|
||||
"info": info,
|
||||
"dag": dag,
|
||||
"validation": validation,
|
||||
"recent_runs": runs,
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -286,6 +286,7 @@ func cmdServer(args []string) {
|
||||
|
||||
executor := NewExecutor(db, cfg.DagsDir)
|
||||
scheduler := NewScheduler(executor, cfg.DagsDir)
|
||||
dagRunHub := NewDagRunHub(db, executor)
|
||||
|
||||
// Prepare frontend FS.
|
||||
var feFS iofs.FS
|
||||
@@ -303,7 +304,7 @@ func cmdServer(args []string) {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
RegisterAPI(mux, executor, scheduler, feFS)
|
||||
RegisterAPI(mux, executor, scheduler, dagRunHub, feFS)
|
||||
|
||||
handler := corsMiddleware(loggingMiddleware(mux))
|
||||
|
||||
|
||||
@@ -2,15 +2,43 @@ package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed migrations/001_init.sql
|
||||
var migrationSQL string
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
// applyMigrations executes every embedded migrations/*.sql in order.
|
||||
// Each statement is idempotent (IF NOT EXISTS / ADD COLUMN). Duplicate-column
|
||||
// errors from re-running ALTER TABLE ADD COLUMN are tolerated.
|
||||
func applyMigrations(conn *sql.DB) error {
|
||||
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Strings(files)
|
||||
for _, f := range files {
|
||||
b, err := migrationsFS.ReadFile(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: read: %w", f, err)
|
||||
}
|
||||
if _, err := conn.Exec(string(b)); err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate column") ||
|
||||
strings.Contains(err.Error(), "already exists") {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s: %w", f, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DB wraps a SQLite connection for DAG run persistence.
|
||||
type DB struct {
|
||||
@@ -24,7 +52,7 @@ func Open(path string) (*DB, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: open %s: %w", path, err)
|
||||
}
|
||||
if _, err := conn.Exec(migrationSQL); err != nil {
|
||||
if err := applyMigrations(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("store: migrate: %w", err)
|
||||
}
|
||||
@@ -36,18 +64,24 @@ func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// Conn exposes the underlying *sql.DB for read-only queries from other
|
||||
// packages (e.g. WS hub in events.go). Do not Close() the returned conn.
|
||||
func (db *DB) Conn() *sql.DB {
|
||||
return db.conn
|
||||
}
|
||||
|
||||
// --- DagRun CRUD ---
|
||||
|
||||
// DagRun mirrors infra.DagRun for the store layer.
|
||||
type DagRun struct {
|
||||
ID string
|
||||
DagName string
|
||||
DagPath string
|
||||
Status string
|
||||
Trigger string
|
||||
StartedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
Error string
|
||||
ID string `json:"id"`
|
||||
DagName string `json:"dag_name"`
|
||||
DagPath string `json:"dag_path"`
|
||||
Status string `json:"status"`
|
||||
Trigger string `json:"trigger"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CreateRun inserts a new run record.
|
||||
@@ -123,17 +157,18 @@ func (db *DB) ListRuns(dagName string, limit, offset int) ([]DagRun, int, error)
|
||||
|
||||
// DagStepResult mirrors infra.DagStepResult for the store layer.
|
||||
type DagStepResult struct {
|
||||
ID string
|
||||
RunID string
|
||||
StepName string
|
||||
Status string
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
StartedAt *time.Time
|
||||
FinishedAt *time.Time
|
||||
DurationMs int64
|
||||
Error string
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepName string `json:"step_name"`
|
||||
FunctionID string `json:"function_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Stdout string `json:"stdout,omitempty"`
|
||||
Stderr string `json:"stderr,omitempty"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// InsertStepResult inserts a new step result.
|
||||
@@ -148,9 +183,9 @@ func (db *DB) InsertStepResult(r *DagStepResult) error {
|
||||
finishedAt = &s
|
||||
}
|
||||
_, err := db.conn.Exec(
|
||||
`INSERT INTO dag_step_results (id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
r.ID, r.RunID, r.StepName, r.Status, r.ExitCode, r.Stdout, r.Stderr,
|
||||
`INSERT INTO dag_step_results (id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
r.ID, r.RunID, r.StepName, r.FunctionID, r.Status, r.ExitCode, r.Stdout, r.Stderr,
|
||||
startedAt, finishedAt, r.DurationMs, r.Error,
|
||||
)
|
||||
return err
|
||||
@@ -173,7 +208,7 @@ func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr s
|
||||
// ListStepResults returns all step results for a given run.
|
||||
func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
|
||||
rows, err := db.conn.Query(
|
||||
`SELECT id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
|
||||
`SELECT id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
|
||||
FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -185,7 +220,7 @@ func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
|
||||
for rows.Next() {
|
||||
var r DagStepResult
|
||||
var startedAt, finishedAt sql.NullString
|
||||
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode,
|
||||
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.FunctionID, &r.Status, &r.ExitCode,
|
||||
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+15
-1
@@ -2,6 +2,7 @@
|
||||
name: shaders_lab
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: 0.1.0
|
||||
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
|
||||
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
|
||||
uses_functions:
|
||||
@@ -30,8 +31,11 @@ uses_types:
|
||||
- dag_types_cpp_gfx
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
dir_path: "cpp/apps/shaders_lab"
|
||||
dir_path: "apps/shaders_lab"
|
||||
repo_url: ""
|
||||
icon:
|
||||
phosphor: "palette"
|
||||
accent: "#ea580c"
|
||||
---
|
||||
|
||||
## Arquitectura
|
||||
@@ -102,3 +106,13 @@ cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w6
|
||||
- El boton "Save as generator" valida snake_case, evita colisionar con
|
||||
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
|
||||
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
|
||||
|
||||
|
||||
## Capability growth log
|
||||
|
||||
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
||||
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
|
||||
- `patch`: bugfix sin cambio observable.
|
||||
|
||||
- v0.1.0 (2026-05-18) — baseline.
|
||||
|
||||
@@ -6,16 +6,21 @@
|
||||
scan_secrets_in_dirty() {
|
||||
local repo_dir="${1:-.}"
|
||||
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
# Accept both regular repos (.git is a directory) and worktrees (.git is a
|
||||
# file containing "gitdir: ..." pointer).
|
||||
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
|
||||
echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Listar archivos modificados o nuevos (excluyendo borrados)
|
||||
# y filtrar por patron de secret en el nombre del archivo
|
||||
# y filtrar por patron de secret en el nombre del archivo.
|
||||
# Excluye extensiones de codigo (sh/go/py/ts/md/etc) para no marcar el
|
||||
# propio scanner ni docs que hablen de "secret"/"token".
|
||||
git -C "$repo_dir" status --porcelain \
|
||||
| awk '{print $NF}' \
|
||||
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
|
||||
| grep -Ev '\.(sh|go|py|ts|tsx|js|jsx|md|rs|cpp|h|hpp|c|java|rb|html|css)$' \
|
||||
|| true
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: deploy_cpp_exe_to_windows
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "deploy_cpp_exe_to_windows(app_name: string, app_dir: string) -> void"
|
||||
description: "Copia el .exe de Windows (compilado por build_cpp_windows) y sus assets al escritorio de Windows /mnt/c/Users/lucas/Desktop/apps/<APP>/. Mata el proceso si esta corriendo (taskkill.exe pre-autorizado), copia DLLs, sincroniza assets/ y enrichers/ con rsync, maneja runtime Python embebido si python_runtime: true en app.md, y copia extras gx-cli. Preserva siempre local_files/ (estado del usuario)."
|
||||
@@ -63,3 +63,8 @@ Desktop/apps/<APP>/
|
||||
- `rsync --delete` en assets/ y enrichers/ para mantener destino limpio.
|
||||
- Si `python_runtime: true` en `app.md` y `runtime/.lock` es mas antiguo que `app.md`, invoca `tools/freeze_python_runtime.sh` automaticamente.
|
||||
- `local_files/` jamas se toca: contiene estado per-PC del usuario (DBs SQLite, ImGui layouts, settings).
|
||||
|
||||
## Capability growth log
|
||||
|
||||
v1.1.0 (2026-05-17) — Bugfix: el `cp` del .exe no chequeaba exit status y la funcion reportaba OK aunque fallase por "Permission denied" (proceso aun vivo). Ahora: (1) tras `taskkill.exe`, poll de hasta 3s sobre `tasklist.exe` esperando muerte real del proceso; (2) `cp` envuelto en retry 5 veces con backoff 0.5s y re-taskkill entre intentos; (3) si los 5 intentos fallan, `return 1` (antes: silently continued).
|
||||
v1.0.0 — Initial.
|
||||
|
||||
@@ -30,12 +30,38 @@ deploy_cpp_exe_to_windows() {
|
||||
mkdir -p "$dest" "$assets"
|
||||
|
||||
# --- 3. Pre-deploy: matar proceso si esta corriendo en Windows ---
|
||||
# Windows libera el file handle async tras taskkill. Hacemos poll hasta que
|
||||
# el proceso desaparezca de tasklist o se agote el timeout.
|
||||
if command -v taskkill.exe >/dev/null 2>&1; then
|
||||
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
|
||||
local i
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if ! tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null \
|
||||
| grep -qi "^${app}.exe"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.3
|
||||
done
|
||||
fi
|
||||
|
||||
# --- 4. Copiar .exe al top level ---
|
||||
cp -v "$exe_src" "$dest/"
|
||||
# --- 4. Copiar .exe al top level con retry ---
|
||||
# Windows puede tener el archivo aun bloqueado momentaneamente; reintentar.
|
||||
local cp_ok=0
|
||||
local attempt
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cp -v "$exe_src" "$dest/"; then
|
||||
cp_ok=1
|
||||
break
|
||||
fi
|
||||
echo "deploy_cpp_exe_to_windows: cp intento $attempt fallo, reintentando..." >&2
|
||||
# Reintentar taskkill por si el proceso resucito o quedo zombie.
|
||||
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
|
||||
sleep 0.5
|
||||
done
|
||||
if [ "$cp_ok" -ne 1 ]; then
|
||||
echo "ERROR: cp del .exe fallo tras 5 intentos. $exe_src -> $dest/" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- 5. DLLs al top level (Windows DLL search convention) ---
|
||||
find "$build_win/apps/$app" -maxdepth 1 -type f -name '*.dll' \
|
||||
|
||||
@@ -17,7 +17,9 @@ git_hook_audit_app_drift() {
|
||||
echo "ERROR: repo_dir required" >&2
|
||||
return 2
|
||||
fi
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
# Accept both regular repos (.git is a directory) and worktrees (.git is a
|
||||
# file containing "gitdir: ..." pointer).
|
||||
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
|
||||
echo "ERROR: $repo_dir is not a git repo" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
@@ -3,7 +3,7 @@ name: launch_cpp_app_windows
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "launch_cpp_app_windows(app_name: string, [desktop_dir: string]) -> void"
|
||||
description: "Lanza un binario .exe en Windows desde WSL2. Asume que deploy_cpp_exe_to_windows ya copió el exe a Desktop/apps/<app_name>/. Usa cmd.exe /c start para desacoplar el proceso y retornar inmediatamente."
|
||||
@@ -68,3 +68,13 @@ launch_cpp_app_windows "registry_dashboard"
|
||||
```
|
||||
|
||||
No se incluyen tests automatizados porque requieren entorno WSL2 con Windows activo y no son automatizables en CI.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Si `FN_REGISTRY_ROOT_WSL` no es tu ruta default de fn_registry (`/home/<user>/fn_registry`), setea la variable antes de invocar esta función: `FN_REGISTRY_ROOT_WSL=/ruta/custom launch_cpp_app_windows <app>`.
|
||||
- El proceso hijo hereda `FN_REGISTRY_ROOT` como path Windows (backslashes) y `FN_REGISTRY_ROOT_WSL` como path Linux. En el exe C++, `py_resolve_interpreter()` usa `FN_REGISTRY_ROOT_WSL` para construir el invocation `wsl.exe -- /path/python3`.
|
||||
- PowerShell escapa `$` con `\$` para evitar expansión de variables en el string del comando.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-05-16) — auto-propaga `FN_REGISTRY_ROOT` (Windows path) + `FN_REGISTRY_ROOT_WSL` (Linux path) al proceso hijo para que pueda invocar WSL python via `wsl.exe`.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# launch_cpp_app_windows — Lanza un .exe en Windows desde WSL2 via cmd.exe /c start.
|
||||
# launch_cpp_app_windows v1.1.0 — Lanza un .exe en Windows desde WSL2 via PowerShell.
|
||||
# Asume que el exe ya fue copiado por deploy_cpp_exe_to_windows al escritorio.
|
||||
# v1.1.0: propaga FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
|
||||
# al proceso hijo para que pueda invocar WSL python via wsl.exe.
|
||||
|
||||
launch_cpp_app_windows() {
|
||||
local app="${1:-}"
|
||||
@@ -26,10 +28,18 @@ launch_cpp_app_windows() {
|
||||
win_app_dir=$(wslpath -w "$desktop_dir/apps/$app")
|
||||
win_exe="$win_app_dir\\$app.exe"
|
||||
|
||||
# Deducir raiz del registry en Linux (WSL) y traducir a Windows path.
|
||||
# FN_REGISTRY_ROOT_WSL puede sobreescribirse en el entorno del llamante.
|
||||
local linux_root win_root
|
||||
linux_root="${FN_REGISTRY_ROOT_WSL:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
|
||||
win_root=$(wslpath -w "$linux_root")
|
||||
|
||||
# Start-Process detacha (equivale a `start` de cmd) y respeta -WorkingDirectory.
|
||||
# Las comillas simples en PowerShell son literales — no procesa \ ni $.
|
||||
# Se inyectan FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
|
||||
# para que el exe pueda localizar el venv WSL y hacer: wsl.exe -- python3 ...
|
||||
powershell.exe -NoProfile -Command \
|
||||
"Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
|
||||
"\$env:FN_REGISTRY_ROOT='$win_root'; \$env:FN_REGISTRY_ROOT_WSL='$linux_root'; Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
|
||||
>/dev/null 2>&1
|
||||
|
||||
local ts
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: refresh_windows_icon_cache
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "refresh_windows_icon_cache() -> void"
|
||||
description: "Fuerza a Windows Explorer a recargar la cache de iconos desde WSL2 via ie4uinit.exe. Best-effort: nunca aborta, retorna 0 si alguna estrategia tuvo exito."
|
||||
tags: [windows, wsl, deploy, shell, icons, cpp-windows]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/refresh_windows_icon_cache.sh"
|
||||
params: []
|
||||
output: "0 si al menos una estrategia tuvo exito, non-zero si todas fallaron. Imprime una linea de estado en stdout."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/refresh_windows_icon_cache.sh
|
||||
refresh_windows_icon_cache
|
||||
# icon cache refresh: ok via ie4uinit -show
|
||||
```
|
||||
|
||||
O directamente via `fn run`:
|
||||
|
||||
```bash
|
||||
./fn run refresh_windows_icon_cache_bash_infra
|
||||
```
|
||||
|
||||
Uso tipico en un pipeline de redeploy tras reconstruir el `.exe`:
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/deploy_cpp_exe_to_windows.sh
|
||||
source bash/functions/infra/refresh_windows_icon_cache.sh
|
||||
|
||||
deploy_cpp_exe_to_windows "registry_dashboard" "apps/registry_dashboard"
|
||||
refresh_windows_icon_cache
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Despues de redeployar un `.exe` Windows cuyo `appicon.ico` cambio (via windres embebido en el build), antes de que Windows muestre el icono nuevo en taskbar, Alt+Tab y File Explorer. Sin esta llamada Windows puede tardar minutos en reflejar el icono actualizado, o no actualizarlo hasta reiniciar Explorer.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `ie4uinit.exe` debe estar en el PATH de WSL2 (normalmente via `/mnt/c/Windows/System32/`). Si Windows esta muy roto puede no encontrarse — la funcion retornara 1 con mensaje de error.
|
||||
- El cambio puede tardar 1-2 segundos en propagarse visualmente despues de que la funcion retorne.
|
||||
- Algunos casos extremos (icono cacheado en el dockable taskbar previamente fijado) requieren desanclar y volver a anclar el ejecutable, o reiniciar `explorer.exe`. Esta funcion no mata Explorer — seria demasiado disruptivo.
|
||||
- Solo funciona desde WSL2 con acceso a herramientas Windows (`/mnt/c/Windows/System32/` en PATH). No tiene efecto en Linux nativo.
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# refresh_windows_icon_cache — Fuerza a Windows Explorer a recargar la cache
|
||||
# de iconos desde WSL2. Best-effort: nunca aborta, retorna 0 si alguna
|
||||
# estrategia tuvo exito.
|
||||
|
||||
refresh_windows_icon_cache() {
|
||||
# Estrategia 1: ie4uinit.exe -show (Windows 10/11 — emite SHCNE_ASSOCCHANGED)
|
||||
if command -v ie4uinit.exe >/dev/null 2>&1; then
|
||||
if ie4uinit.exe -show >/dev/null 2>&1; then
|
||||
echo "icon cache refresh: ok via ie4uinit -show"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Estrategia 2: ie4uinit.exe -ClearIconCache (fallback para builds viejos)
|
||||
if ie4uinit.exe -ClearIconCache >/dev/null 2>&1; then
|
||||
echo "icon cache refresh: ok via ie4uinit -ClearIconCache"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "icon cache refresh: failed (ie4uinit.exe not found or all strategies failed)"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
refresh_windows_icon_cache "$@"
|
||||
fi
|
||||
@@ -3,11 +3,11 @@ name: resolve_cpp_app_dir
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "resolve_cpp_app_dir(app_name?: string) -> stdout: app_name\tapp_dir"
|
||||
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en ambas ubicaciones. Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
|
||||
tags: [cpp, resolve, app, directory, infra]
|
||||
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en las tres ubicaciones (apps/ canonical issue 0096 primero, luego cpp/apps/ legacy, luego projects/*/apps/). Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
|
||||
tags: [cpp, resolve, app, directory, infra, cpp-windows]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -20,7 +20,7 @@ test_file_path: ""
|
||||
file_path: "bash/functions/infra/resolve_cpp_app_dir.sh"
|
||||
params:
|
||||
- name: app_name
|
||||
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de cpp/apps/<X>/ o projects/*/apps/<X>/."
|
||||
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/."
|
||||
output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En caso de error imprime ayuda a stderr y sale con exit 1."
|
||||
---
|
||||
|
||||
@@ -44,4 +44,13 @@ APP_DIR="$(echo "$resolved" | cut -f2)"
|
||||
|
||||
## Notas
|
||||
|
||||
Busca en orden: primero `$ROOT/cpp/apps/<X>`, luego `$ROOT/projects/*/apps/<X>` (primer match gana). Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente.
|
||||
Busca en orden:
|
||||
1. `$ROOT/apps/<X>` con `CMakeLists.txt` — layout canonical post-issue 0096.
|
||||
2. `$ROOT/cpp/apps/<X>` — legacy pre-issue 0096.
|
||||
3. `$ROOT/projects/*/apps/<X>` — apps de un proyecto (primer match gana).
|
||||
|
||||
Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente. Helper interno `_list_cpp_apps` evita duplicar codigo en los paths de error.
|
||||
|
||||
### Growth log
|
||||
|
||||
- v1.1.0 (2026-05-16) — busca tambien en `apps/<X>/` (canonical issue 0096). Antes solo cubria `cpp/apps/<X>/` y `projects/*/apps/<X>/`, lo que hacia que `./fn run compile_cpp_app <name>` fallara para apps movidas al layout canonical (ej. `dag_engine_ui`).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# resolve_cpp_app_dir — Resuelve nombre y directorio absoluto de una app C++ del registry.
|
||||
# Sin arg: deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/.
|
||||
# Con arg: usa el nombre directamente y busca en ambas ubicaciones.
|
||||
# Sin arg: deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/.
|
||||
# Con arg: usa el nombre directamente y busca en las tres ubicaciones.
|
||||
# Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0.
|
||||
# Error: lista apps disponibles en stderr + exit 1.
|
||||
|
||||
@@ -9,18 +9,28 @@ resolve_cpp_app_dir() {
|
||||
local app_arg="${1:-}"
|
||||
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
|
||||
_list_cpp_apps() {
|
||||
ls "$root/apps/" 2>/dev/null | sed 's/^/ apps\//'
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
}
|
||||
|
||||
# --- Deducir desde CWD si no hay argumento ---
|
||||
if [ -z "$app_arg" ]; then
|
||||
local cwd
|
||||
cwd="$(pwd)"
|
||||
case "$cwd" in
|
||||
"$root"/apps/*/|"$root"/apps/*)
|
||||
local rel="${cwd#"$root/apps/"}"
|
||||
app_arg="${rel%%/*}"
|
||||
;;
|
||||
"$root"/cpp/apps/*/|"$root"/cpp/apps/*)
|
||||
# Extraer primer segmento tras cpp/apps/
|
||||
local rel="${cwd#"$root/cpp/apps/"}"
|
||||
app_arg="${rel%%/*}"
|
||||
;;
|
||||
"$root"/projects/*/apps/*/|"$root"/projects/*/apps/*)
|
||||
# Extraer primer segmento tras la ultima /apps/
|
||||
local rel="${cwd#"$root/projects/"}"
|
||||
rel="${rel#*/apps/}"
|
||||
app_arg="${rel%%/*}"
|
||||
@@ -33,12 +43,7 @@ resolve_cpp_app_dir() {
|
||||
echo "ERROR: no se pudo deducir la app desde el directorio actual." >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
{
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
} >&2
|
||||
_list_cpp_apps >&2
|
||||
echo "" >&2
|
||||
echo "Uso: resolve_cpp_app_dir <app_name>" >&2
|
||||
return 1
|
||||
@@ -47,12 +52,17 @@ resolve_cpp_app_dir() {
|
||||
# --- Buscar directorio real ---
|
||||
local app_dir=""
|
||||
|
||||
# Primero: cpp/apps/<X>
|
||||
if [ -d "$root/cpp/apps/$app_arg" ]; then
|
||||
# Primero (issue 0096 canonical): apps/<X>
|
||||
if [ -d "$root/apps/$app_arg" ] && [ -f "$root/apps/$app_arg/CMakeLists.txt" ]; then
|
||||
app_dir="$root/apps/$app_arg"
|
||||
fi
|
||||
|
||||
# Segundo (legacy): cpp/apps/<X>
|
||||
if [ -z "$app_dir" ] && [ -d "$root/cpp/apps/$app_arg" ]; then
|
||||
app_dir="$root/cpp/apps/$app_arg"
|
||||
fi
|
||||
|
||||
# Segundo: projects/*/apps/<X> (primer match)
|
||||
# Tercero: projects/*/apps/<X> (primer match)
|
||||
if [ -z "$app_dir" ]; then
|
||||
for cand in "$root"/projects/*/apps/"$app_arg"; do
|
||||
if [ -d "$cand" ]; then
|
||||
@@ -63,15 +73,10 @@ resolve_cpp_app_dir() {
|
||||
fi
|
||||
|
||||
if [ -z "$app_dir" ]; then
|
||||
echo "ERROR: no se encuentra app '$app_arg' en cpp/apps/ ni en projects/*/apps/" >&2
|
||||
echo "ERROR: no se encuentra app '$app_arg' en apps/, cpp/apps/ ni en projects/*/apps/" >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
{
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
} >&2
|
||||
_list_cpp_apps >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: fn_sync_with_pass
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "fn_sync_with_pass [status|locations|<args>...]"
|
||||
description: "Wrapper de fn sync que lee credenciales del password-store pass y exporta FN_REGISTRY_API y REGISTRY_API_TOKEN antes de invocar el CLI. Evita persistir secretos en ~/.zshrc."
|
||||
tags: [sync, registry, pass, gpg, launcher]
|
||||
uses_functions:
|
||||
- pass_get_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: "registry/basicauth-user"
|
||||
desc: "Entry de pass con el usuario para basicAuth del registry API (linea 1)"
|
||||
- name: "registry/basicauth-pass"
|
||||
desc: "Entry de pass con la contraseña para basicAuth del registry API (linea 1)"
|
||||
- name: "registry/api-token"
|
||||
desc: "Entry de pass con el REGISTRY_API_TOKEN (linea 1)"
|
||||
- name: "args"
|
||||
desc: "Argumentos opcionales forwarded a fn sync: status, locations, o nada para push+pull completo"
|
||||
output: "Mismo output que ./fn sync (stdin/stdout/stderr heredados). Exit code del subproceso fn sync."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/fn_sync_with_pass.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Sync simple (push+pull completo)
|
||||
./fn run fn_sync_with_pass_bash_pipelines
|
||||
|
||||
# Ver estado local: PC, API, conteos
|
||||
./fn run fn_sync_with_pass_bash_pipelines status
|
||||
|
||||
# Mapa de ubicaciones cross-PC
|
||||
./fn run fn_sync_with_pass_bash_pipelines locations
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites ejecutar `fn sync` sin tener las credenciales exportadas en el entorno. Sustituye al bloque de `export FN_REGISTRY_API=...` que de otro modo habria que poner en `~/.zshrc`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Si GPG no tiene la clave desbloqueada, `pass show` abre el prompt del agente gpg. Dejarlo pasar — no capturar stderr para no interferir con el pinentry.
|
||||
- Requiere que el password-store este inicializado (`pass init`). Si no existe, `pass show` falla con error claro.
|
||||
- `FN_REGISTRY_ROOT` debe apuntar a la raiz del registry donde vive el binario `./fn`. Si no esta seteado, se resuelve via `git rev-parse --show-toplevel`.
|
||||
- Los tres entries de pass deben tener el valor en la **linea 1** (convencion estandar de pass). Metadata adicional en lineas siguientes es ignorada.
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# fn_sync_with_pass — Wrapper de fn sync que lee credenciales desde pass.
|
||||
set -euo pipefail
|
||||
|
||||
FN_ROOT="${FN_REGISTRY_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
||||
|
||||
fn_sync_with_pass() {
|
||||
command -v pass >/dev/null 2>&1 || {
|
||||
echo "fn_sync_with_pass: 'pass' CLI no instalado. Instala con: apt install pass" >&2
|
||||
return 127
|
||||
}
|
||||
|
||||
local u p t
|
||||
|
||||
u=$(pass show registry/basicauth-user 2>/dev/null | head -n1) || {
|
||||
echo "fn_sync_with_pass: falta registry/basicauth-user en pass. Crea con: pass insert registry/basicauth-user" >&2
|
||||
return 1
|
||||
}
|
||||
p=$(pass show registry/basicauth-pass 2>/dev/null | head -n1) || {
|
||||
echo "fn_sync_with_pass: falta registry/basicauth-pass en pass. Crea con: pass insert registry/basicauth-pass" >&2
|
||||
return 1
|
||||
}
|
||||
t=$(pass show registry/api-token 2>/dev/null | head -n1) || {
|
||||
echo "fn_sync_with_pass: falta registry/api-token en pass. Crea con: pass insert registry/api-token" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
export FN_REGISTRY_API="https://${u}:${p}@registry.organic-machine.com"
|
||||
export REGISTRY_API_TOKEN="$t"
|
||||
|
||||
cd "$FN_ROOT"
|
||||
./fn sync "$@"
|
||||
}
|
||||
|
||||
# Ejecucion directa (no library mode)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
fn_sync_with_pass "$@"
|
||||
fi
|
||||
@@ -3,11 +3,11 @@ name: init_cpp_app
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "0.1.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "init_cpp_app(name: string, [--project <p>] [--domain <d>] [--desc <s>] [--tags <csv>]) -> void"
|
||||
description: "Scaffolder estandar de apps C++ del registry. Genera main.cpp + CMakeLists.txt + app.md siguiendo el patron canonico (cfg.about/log/panels, sin app_menubar manual, dockspace via framework), registra la app en cpp/CMakeLists.txt, inicializa repo Gitea dataforge/<name> y ejecuta fn index."
|
||||
tags: [cpp, imgui, scaffold, pipeline, bash, launcher]
|
||||
tags: [cpp, imgui, scaffold, pipeline, bash, launcher, cpp-tables]
|
||||
uses_functions:
|
||||
- ensure_repo_synced_bash_infra
|
||||
uses_types: []
|
||||
@@ -47,9 +47,9 @@ fn run init_cpp_app finance_panel --project budget --desc "Panel de finanzas" --
|
||||
|
||||
```
|
||||
<dir>/
|
||||
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render)
|
||||
CMakeLists.txt # add_imgui_app(<name> main.cpp)
|
||||
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url)
|
||||
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render) + data_table comentado
|
||||
CMakeLists.txt # add_imgui_app(<name> main.cpp) + target_link_libraries fn_table_viz (con guard)
|
||||
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url) + uses_functions comentados
|
||||
```
|
||||
|
||||
Y ademas:
|
||||
@@ -66,9 +66,31 @@ La plantilla cumple `cpp/PATTERNS.md`:
|
||||
- NO llama `DockSpaceOverViewport` (auto_dockspace=true por defecto).
|
||||
- Declara `panels[]` con un panel "Main" toggleable.
|
||||
- Setea `cfg.about` (window About) y `cfg.log` (logger + ventana Logs).
|
||||
- Include `viz/data_table.h` comentado + panel "Data" comentado en `render()` — descomentar para activar `data_table::render()`.
|
||||
|
||||
## Activar data_table::render()
|
||||
|
||||
1. En `main.cpp`: descomentar `#include "viz/data_table.h"` y el bloque del panel Data en `render()`.
|
||||
2. En `app.md`: descomentar los 12 IDs del stack `fn_table_viz` en `uses_functions`.
|
||||
3. El `CMakeLists.txt` ya linka `fn_table_viz` via guard `if(TARGET fn_table_viz)` — sin cambio manual.
|
||||
4. Poblar `data_tables` con tus `data_table::TableInput` y el panel aparece en el DockSpace.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites crear una app C++ nueva que siga el patron canonico del registry. Es el unico camino autorizado para crear apps en `cpp/apps/` o `projects/*/apps/` — nunca escribir `main.cpp` + `CMakeLists.txt` a mano.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Si `GITEA_URL`/`GITEA_TOKEN` no estan seteados, solo hace `git init` local (no crea repo remoto).
|
||||
- `fn_table_viz` requiere que `vendor/lua` este presente en `cpp/`; el guard `if(TARGET fn_table_viz)` evita errores de link si no esta disponible.
|
||||
- El bloque de `uses_functions` en `app.md` queda comentado intencionalmente — descomenta solo las funciones que la app realmente use para mantener el grafo de dependencias limpio.
|
||||
|
||||
## Despues de crear
|
||||
|
||||
1. Editar `app.md` y completar `uses_functions` cuando la app consuma funciones del registry.
|
||||
2. Anadir las funciones del registry al `CMakeLists.txt` como paths absolutos: `${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp`.
|
||||
1. Si usas `data_table::render()`: descomentar include + panel en `main.cpp`, descomentar IDs en `app.md`, ejecutar `fn index`.
|
||||
2. Para otras funciones del registry: anadir paths absolutos en `CMakeLists.txt` y los IDs en `uses_functions` de `app.md`.
|
||||
3. Build: `cd cpp && cmake --build build --target <name> -j`.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
v1.1.0 (2026-05-15) — Auto-wires fn_table_viz; new apps get target_link_libraries + commented data_table template.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# Uso:
|
||||
# init_cpp_app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"]
|
||||
#
|
||||
# Por defecto domain=tools, sin proyecto (cpp/apps/<name>/).
|
||||
# Por defecto domain=tools, sin proyecto (apps/<name>/, issue 0096).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -55,7 +55,7 @@ init_cpp_app() {
|
||||
fi
|
||||
rel_dir="projects/$project/apps/$name"
|
||||
else
|
||||
rel_dir="cpp/apps/$name"
|
||||
rel_dir="apps/$name"
|
||||
fi
|
||||
abs_dir="$FN_ROOT/$rel_dir"
|
||||
|
||||
@@ -69,9 +69,11 @@ init_cpp_app() {
|
||||
# ---------- main.cpp ----------
|
||||
cat > "$abs_dir/main.cpp" <<EOF
|
||||
#include <imgui.h>
|
||||
#include "framework/app_base.h"
|
||||
#include "app_base.h"
|
||||
#include "core/panel_menu.h"
|
||||
#include "core/icons_tabler.h"
|
||||
#include "core/logger.h"
|
||||
// #include "viz/data_table.h" // uncomment to enable data_table::render() panel
|
||||
|
||||
// Toggles de paneles (visibles desde el menu View del menubar canonico)
|
||||
static bool g_show_main = true;
|
||||
@@ -90,6 +92,11 @@ static void render() {
|
||||
// DockSpaceOverViewport central (auto_dockspace=true por defecto).
|
||||
// Aqui solo se dibujan los paneles propios de la app.
|
||||
if (g_show_main) draw_main();
|
||||
|
||||
// === Data panel (uncomment to enable) ===
|
||||
// static data_table::State data_state;
|
||||
// static std::vector<data_table::TableInput> data_tables; // populate from your source
|
||||
// data_table::render("main_data", data_tables, data_state);
|
||||
}
|
||||
|
||||
int main(int /*argc*/, char** /*argv*/) {
|
||||
@@ -115,6 +122,12 @@ add_imgui_app($name
|
||||
)
|
||||
target_include_directories($name PRIVATE \${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# fn_table_viz: provides data_table::render(), viz_render, TQL engine, Lua, LLM.
|
||||
# Guard keeps the app compilable in builds where vendor/lua is absent.
|
||||
if(TARGET fn_table_viz)
|
||||
target_link_libraries($name PRIVATE fn_table_viz)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties($name PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
endif()
|
||||
@@ -135,7 +148,20 @@ lang: cpp
|
||||
domain: $domain
|
||||
description: "$desc"
|
||||
tags: $tags_yaml
|
||||
uses_functions: []
|
||||
uses_functions:
|
||||
# Uncomment when using data_table::render() — provided via fn_table_viz:
|
||||
# - data_table_cpp_viz
|
||||
# - viz_render_cpp_viz
|
||||
# - compute_stage_cpp_core
|
||||
# - compute_pipeline_cpp_core
|
||||
# - compute_column_stats_cpp_core
|
||||
# - auto_detect_type_cpp_core
|
||||
# - tql_emit_cpp_core
|
||||
# - tql_apply_cpp_core
|
||||
# - lua_engine_cpp_core
|
||||
# - join_tables_cpp_core
|
||||
# - tql_to_sql_cpp_core
|
||||
# - llm_anthropic_cpp_core
|
||||
uses_types: []
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
@@ -175,11 +201,14 @@ if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
|
||||
endif()
|
||||
EOF
|
||||
else
|
||||
local upper
|
||||
upper="$(echo "$name" | tr '[:lower:]' '[:upper:]')"
|
||||
cat >> "$cpp_cmake" <<EOF
|
||||
|
||||
# --- $name ---
|
||||
if(EXISTS \${CMAKE_CURRENT_SOURCE_DIR}/apps/$name/CMakeLists.txt)
|
||||
add_subdirectory(apps/$name)
|
||||
# --- $name (lives in apps/, issue 0096) ---
|
||||
set(_${upper}_DIR \${CMAKE_SOURCE_DIR}/../apps/$name)
|
||||
if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(\${_${upper}_DIR} \${CMAKE_BINARY_DIR}/apps/$name)
|
||||
endif()
|
||||
EOF
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: redeploy_all_cpp_apps
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "redeploy_all_cpp_apps(filter?: string) -> void"
|
||||
description: "Cross-compila TODOS los apps C++ del registry en un solo cmake pass y despliega cada .exe al Desktop de Windows. Mas rapido que N builds individuales. Acepta filtro de nombre para despliegue parcial."
|
||||
tags: [cpp, windows, deploy, redeploy, bulk, cpp-windows]
|
||||
uses_functions:
|
||||
- build_cpp_windows_bash_infra
|
||||
- deploy_cpp_exe_to_windows_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/redeploy_all_cpp_apps.sh"
|
||||
params:
|
||||
- name: filter
|
||||
desc: "Opcional. Substring para limitar el deploy a apps cuyo nombre lo contenga (ej: 'graph' solo despliega apps con 'graph' en el nombre). Sin valor = todas las apps."
|
||||
output: "Imprime tabla resumen con OK/SKIPPED/FAILED y nombres de cada app. Exit 1 si al menos una app fallo el deploy."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Recompilar y redesplegar TODAS las apps C++ tras un cambio en cpp/framework/
|
||||
./fn run redeploy_all_cpp_apps
|
||||
|
||||
# Solo apps cuyo nombre contenga "graph"
|
||||
./fn run redeploy_all_cpp_apps graph
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras un cambio en `cpp/framework/app_base.cpp`, `cpp/functions/core/*` o cualquier
|
||||
funcion linkada a multiples apps. Ahorra correr `redeploy_cpp_app_windows <name> <dir>`
|
||||
N veces — un solo cmake pass compila todo el arbol en paralelo.
|
||||
|
||||
## Comportamiento
|
||||
|
||||
1. **Build**: invoca `build_cpp_windows` sin argumento (compila todo el arbol con
|
||||
`-j$(nproc)`). Un solo cmake pass — mucho mas rapido que N builds individuales.
|
||||
2. **Descubrimiento**: itera `apps/*/CMakeLists.txt` y `projects/*/apps/*/CMakeLists.txt`.
|
||||
**No** usa `cpp/apps/` (deprecado tras issue 0096).
|
||||
3. **Filtro** (opcional): si se paso un argumento, solo procesa apps cuyo `basename`
|
||||
contiene el substring.
|
||||
4. **Por cada app**:
|
||||
- Localiza `.exe` en `cpp/build/windows/apps/<name>/<name>.exe`; si no existe,
|
||||
busca bajo `cpp/build/windows/` como fallback.
|
||||
- Si no hay `.exe`: log SKIP, continua (no aborta — apps headless o sub-repos no
|
||||
clonados no tienen build target).
|
||||
- `taskkill.exe /IM <name>.exe /F` silencioso (no aborta si falla).
|
||||
- `deploy_cpp_exe_to_windows <name> <app_dir>` (copia exe + DLLs + assets +
|
||||
enrichers + runtime, preserva `local_files/`).
|
||||
- Error por app: log FAILED, continua con la siguiente.
|
||||
5. **Resumen final**: tabla `OK / SKIPPED / FAILED` con nombres. Exit 1 si hay
|
||||
al menos un FAILED.
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
| Variable | Default | Descripcion |
|
||||
|---|---|---|
|
||||
| `FN_REGISTRY_ROOT` | auto-detect | Raiz del registry (busca hacia arriba desde el script) |
|
||||
| `BUILD_WIN` | `$root/cpp/build/windows` | Directorio de build Windows |
|
||||
| `WIN_DESKTOP_APPS` | `/mnt/c/Users/lucas/Desktop/apps` | Destino de deploy en Windows |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Solo Windows (cross-compile mingw-w64 + Desktop deploy via WSL2). En Linux puro no aplica.
|
||||
- `taskkill.exe` requiere WSL2 con interop habilitado. No funciona en WSL1 ni Linux nativo.
|
||||
- Algunas apps pueden no estar en el grafo cmake actual (sub-repo no clonado, `add_subdirectory`
|
||||
protegido por `if(EXISTS ...)`). El pipeline las SKIPea sin abortar — comportamiento esperado.
|
||||
- Build paralelo puede consumir varios GB de RAM. Si hay OOM, reducir paralelismo exportando
|
||||
`BUILD_JOBS=4` antes de invocar (actualmente la funcion `build_cpp_windows` usa `$(nproc)`;
|
||||
si necesitas override edita `BUILD_JOBS` como variable de entorno custom o fork la funcion).
|
||||
- El loop de deploy atrapa errores por app (`|| { failed+=...; continue; }`) para no abortar
|
||||
en el primer fallo — todas las apps se intentan aunque alguna falle.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.0.0 (2026-05-16) — creacion. Tras issue 0096 (apps movidas a `apps/<X>/`) el patron "recompilar+desplegar todas tras un cambio en `cpp/framework/`" se repitio varias veces sin un wrapper. Pipeline tolerante a fallos: build best-effort (test_* roto en mingw no aborta), deploy por app captura fallos individuales, summary OK/SKIPPED/FAILED al final. Primera corrida real (16 May 2026): 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED.
|
||||
|
||||
## Notas operativas (2026-05-16)
|
||||
|
||||
- `build_cpp_windows` sin arg compila el arbol entero. Si hay targets rotos (ej. `test_llm_anthropic`, `test_graph_icons` usan `setenv()` no disponible en mingw-w64), el pipeline logea `[1/2] Build returned exit=N — continuing with deploy of available exes` y sigue con la fase de deploy. Cada app sin `.exe` queda SKIPPED.
|
||||
- Tras una corrida exitosa, los `.exe` quedan en `/mnt/c/Users/lucas/Desktop/apps/<name>/<name>.exe`. Lanzar individualmente con `./fn run is_cpp_app_running_windows <name>` para chequear y `launch_cpp_app_windows <name>` para arrancar.
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# redeploy_all_cpp_apps — Cross-compila TODOS los apps C++ del registry en un solo
|
||||
# cmake pass y despliega cada .exe al Desktop de Windows.
|
||||
# Uso: redeploy_all_cpp_apps [filter]
|
||||
# filter substring opcional para limitar el deploy a apps cuyo nombre lo contenga
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../infra/build_cpp_windows.sh"
|
||||
source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh"
|
||||
|
||||
redeploy_all_cpp_apps() {
|
||||
local filter="${1:-}"
|
||||
|
||||
# --- Localizar raiz del registry ---
|
||||
local root="${FN_REGISTRY_ROOT:-}"
|
||||
if [ -z "$root" ]; then
|
||||
local d="$SCRIPT_DIR"
|
||||
while [ "$d" != "/" ]; do
|
||||
if [ -f "$d/registry.db" ] && [ -d "$d/cpp" ]; then
|
||||
root="$d"; break
|
||||
fi
|
||||
d="$(dirname "$d")"
|
||||
done
|
||||
fi
|
||||
if [ -z "$root" ]; then
|
||||
echo "[redeploy_all_cpp_apps] ERROR: no se localiza la raiz del registry. Exporta FN_REGISTRY_ROOT." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
|
||||
|
||||
# --- Paso 1: compilar TODO el arbol (un solo cmake pass) ---
|
||||
# Tolerante a fallos: si algun target (ej. test_* roto en mingw, app con
|
||||
# bug puntual) falla, los demas exes que SI se construyeron siguen siendo
|
||||
# desplegables. El loop de deploy hace SKIP por cada app sin .exe, asi que
|
||||
# el modo "build best-effort + deploy lo que haya" es seguro.
|
||||
echo "[1/2] Cross-compiling all C++ targets (best-effort)..."
|
||||
local build_rc=0
|
||||
build_cpp_windows || build_rc=$?
|
||||
if [ "$build_rc" -ne 0 ]; then
|
||||
echo "[1/2] Build returned exit=$build_rc — continuing with deploy of available exes" >&2
|
||||
else
|
||||
echo "[1/2] Build OK"
|
||||
fi
|
||||
|
||||
# --- Descubrir apps con CMakeLists.txt ---
|
||||
# Busca en apps/*/ y projects/*/apps/*/ (no en cpp/apps/ — deprecado)
|
||||
local -a app_dirs=()
|
||||
while IFS= read -r cmakelists; do
|
||||
app_dirs+=("$(dirname "$cmakelists")")
|
||||
done < <(
|
||||
find "$root/apps" -maxdepth 2 -name "CMakeLists.txt" 2>/dev/null | sort
|
||||
find "$root/projects" -maxdepth 4 -path "*/apps/*/CMakeLists.txt" 2>/dev/null | sort
|
||||
)
|
||||
|
||||
if [ ${#app_dirs[@]} -eq 0 ]; then
|
||||
echo "[redeploy_all_cpp_apps] WARN: no se encontraron apps con CMakeLists.txt" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
# --- Paso 2: deploy por app ---
|
||||
echo "[2/2] Deploying apps to Windows Desktop..."
|
||||
local -a ok=() skipped=() failed=()
|
||||
|
||||
for app_dir in "${app_dirs[@]}"; do
|
||||
local name
|
||||
name="$(basename "$app_dir")"
|
||||
|
||||
# Aplicar filtro si se indico
|
||||
if [ -n "$filter" ] && [[ "$name" != *"$filter"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Localizar el .exe en la ubicacion canonica
|
||||
local exe_path="$build_win/apps/$name/$name.exe"
|
||||
if [ ! -f "$exe_path" ]; then
|
||||
# Fallback: buscar bajo build_win/
|
||||
exe_path="$(find "$build_win" -name "$name.exe" -type f 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$exe_path" ] || [ ! -f "$exe_path" ]; then
|
||||
echo " SKIP: $name — .exe no encontrado en $build_win" >&2
|
||||
skipped+=("$name")
|
||||
continue
|
||||
fi
|
||||
|
||||
# taskkill silencioso (pre-autorizado; deploy_cpp_exe_to_windows lo hace internamente,
|
||||
# pero si deploy falla antes de llegar ahi nos aseguramos de liberar el lock)
|
||||
if command -v taskkill.exe >/dev/null 2>&1; then
|
||||
taskkill.exe /IM "${name}.exe" /F >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if deploy_cpp_exe_to_windows "$name" "$app_dir"; then
|
||||
ok+=("$name")
|
||||
else
|
||||
echo " FAILED: $name" >&2
|
||||
failed+=("$name")
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Resumen ---
|
||||
echo ""
|
||||
echo "===== redeploy_all_cpp_apps — summary ====="
|
||||
printf " OK : %d\n" "${#ok[@]}"
|
||||
printf " SKIPPED : %d\n" "${#skipped[@]}"
|
||||
printf " FAILED : %d\n" "${#failed[@]}"
|
||||
|
||||
if [ ${#ok[@]} -gt 0 ]; then
|
||||
echo " Deployed:"
|
||||
for n in "${ok[@]}"; do printf " + %s\n" "$n"; done
|
||||
fi
|
||||
if [ ${#skipped[@]} -gt 0 ]; then
|
||||
echo " Skipped (no .exe):"
|
||||
for n in "${skipped[@]}"; do printf " - %s\n" "$n"; done
|
||||
fi
|
||||
if [ ${#failed[@]} -gt 0 ]; then
|
||||
echo " Failed:"
|
||||
for n in "${failed[@]}"; do printf " x %s\n" "$n"; done
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente (fn run lo invoca como script)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
redeploy_all_cpp_apps "$@"
|
||||
fi
|
||||
@@ -3,16 +3,17 @@ name: redeploy_cpp_app_windows
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "redeploy_cpp_app_windows(app_name: string, app_dir: string, [--build]) -> void"
|
||||
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify."
|
||||
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify e incluye refresh del icon cache del shell."
|
||||
tags: [cpp, windows, redeploy, pipeline, wsl, launcher, cpp-windows]
|
||||
uses_functions:
|
||||
- build_cpp_windows_bash_infra
|
||||
- deploy_cpp_exe_to_windows_bash_infra
|
||||
- launch_cpp_app_windows_bash_infra
|
||||
- is_cpp_app_running_windows_bash_infra
|
||||
- refresh_windows_icon_cache_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
@@ -47,6 +48,7 @@ redeploy_cpp_app_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_de
|
||||
1. **Parsear flag `--build`** (default off, opt-in).
|
||||
2. **Si `--build`**: invocar `build_cpp_windows <app_name>` para compilar `cpp/build/windows/apps/<app_name>/<app_name>.exe`. Si falla, exit 1 sin tocar el Desktop.
|
||||
3. **Deploy**: invocar `deploy_cpp_exe_to_windows "<app_name>" "<app_dir>"`. Esta función mata el proceso si está vivo (taskkill.exe pre-autorizado), copia exe + DLLs + assets + runtime + enrichers, y preserva `local_files/`.
|
||||
3b. **Refresh icon cache** (v1.1.0+): invocar `refresh_windows_icon_cache` (best-effort). Llama `ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar al timestamp. Si falla, no aborta el pipeline.
|
||||
4. **Launch**: invocar `launch_cpp_app_windows "<app_name>"` para arrancar la app en Windows.
|
||||
5. **Wait**: `sleep 1` — espera arranque corto.
|
||||
6. **Verify**: invocar `is_cpp_app_running_windows "<app_name>"`. Si NO está vivo → exit 1 con mensaje claro.
|
||||
|
||||
@@ -7,6 +7,7 @@ source "$SCRIPT_DIR/../infra/build_cpp_windows.sh"
|
||||
source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh"
|
||||
source "$SCRIPT_DIR/../infra/launch_cpp_app_windows.sh"
|
||||
source "$SCRIPT_DIR/../infra/is_cpp_app_running_windows.sh"
|
||||
source "$SCRIPT_DIR/../infra/refresh_windows_icon_cache.sh"
|
||||
|
||||
redeploy_cpp_app_windows() {
|
||||
local app_name=""
|
||||
@@ -63,6 +64,12 @@ redeploy_cpp_app_windows() {
|
||||
fi
|
||||
echo "[2/4] Deploy OK"
|
||||
|
||||
# Refrescar cache de iconos del shell. Sin esto el .exe nuevo puede salir
|
||||
# con el icono generico (Windows cachea por timestamp/path en iconcache.db
|
||||
# y a veces no detecta el cambio inmediatamente). Best-effort: si falla
|
||||
# no abortamos el redeploy.
|
||||
refresh_windows_icon_cache || true
|
||||
|
||||
# Paso 3: lanzar la app
|
||||
echo "[3/4] Launching $app_name..."
|
||||
if ! launch_cpp_app_windows "$app_name"; then
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: refresh_app_hub
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "refresh_app_hub([--hub-dir <path>] [--size <px>] [--no-restart] [--style <s>]) -> void"
|
||||
description: "Pipeline orquestador que regenera los iconos PNG y el manifest TSV del App Hub desde registry.db y reinicia el proceso app_hub_launcher en Windows. Cubre el ciclo completo: export icons → export manifest → taskkill → relaunch. Default style=white_duotone (duotone Phosphor blanco sobre bg accent). Override con --style fill_white | adaptive_duotone."
|
||||
tags: [hub, launcher, icons, manifest, cpp, windows, wsl, cpp-windows, deploy]
|
||||
uses_functions:
|
||||
- export_hub_icons_py_infra
|
||||
- export_hub_manifest_py_infra
|
||||
- is_cpp_app_running_windows_bash_infra
|
||||
- launch_cpp_app_windows_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/refresh_app_hub.sh"
|
||||
params:
|
||||
- name: "--hub-dir"
|
||||
desc: "Directorio local_files del hub en Windows (accesible desde WSL). Default: /mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files. Los PNGs se escriben en <hub-dir>/icons/ y el manifest en <hub-dir>/hub_manifest.tsv."
|
||||
- name: "--size"
|
||||
desc: "Lado en pixels de cada PNG de icono. Default 64. Pasar 128 para pantallas HiDPI."
|
||||
- name: "--no-restart"
|
||||
desc: "Si está presente, solo regenera icons + manifest sin tocar el proceso del hub (pasos 3 y 4 se marcan como skipped). Útil para actualizar los archivos mientras el hub está cerrado manualmente."
|
||||
output: "Imprime resumen de 4 pasos (icons exported, manifest rows, hub killed/skipped, hub launched) y finaliza con 'OK: app_hub refreshed'. Exit 1 si falla cualquier paso, con mensaje indicando cuál."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Caso habitual: regenerar todo y reiniciar el hub
|
||||
./fn run refresh_app_hub
|
||||
|
||||
# Solo regenerar iconos y manifest, sin tocar el proceso
|
||||
./fn run refresh_app_hub --no-restart
|
||||
|
||||
# Iconos a 128px para pantalla HiDPI o hub-dir personalizado
|
||||
./fn run refresh_app_hub --size 128
|
||||
./fn run refresh_app_hub --hub-dir /mnt/d/MiDesktop/apps/app_hub_launcher/local_files
|
||||
|
||||
# Combinado
|
||||
./fn run refresh_app_hub --size 128 --hub-dir /mnt/d/MiDesktop/apps/app_hub_launcher/local_files
|
||||
```
|
||||
|
||||
Salida esperada:
|
||||
|
||||
```
|
||||
[1/4] Exporting PNG icons (size=64px) → /mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files/icons ...
|
||||
[1/4] PNG icons exported: 12
|
||||
[2/4] Exporting manifest → .../hub_manifest.tsv ...
|
||||
[2/4] Manifest exported: 12 rows
|
||||
[3/4] Hub running → killing app_hub_launcher.exe ...
|
||||
[3/4] Hub running → killed
|
||||
[4/4] Launching app_hub_launcher ...
|
||||
[4/4] Hub launched
|
||||
OK: app_hub refreshed
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Ejecutar este pipeline después de:
|
||||
- Cambiar el bloque `icon:` (`phosphor`, `accent`) en cualquier `app.md` C++.
|
||||
- Modificar la `description` de una app C++ (se refleja en el manifest TSV).
|
||||
- Añadir una app nueva con `lang: cpp` y `framework: imgui` al registry.
|
||||
- Cambiar el `name` de una app imgui (el PNG y la fila TSV usan el `name`).
|
||||
|
||||
En todos esos casos el hub cachea los archivos al arrancar, así que el reinicio es obligatorio para ver los cambios.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Cache al arrancar**: `app_hub_launcher` carga `local_files/icons/*.png` y `local_files/hub_manifest.tsv` una sola vez al inicio. Sin el reinicio los cambios no se ven aunque los archivos estén actualizados. Usar `--no-restart` solo si el hub está cerrado o si quieres preparar los archivos antes de lanzarlo a mano.
|
||||
- **Paths Windows requieren WSL `/mnt/c/`**: el `--hub-dir` debe ser una ruta accesible desde WSL2. Las rutas `C:\...` nativas no funcionan en Bash — convertirlas con `wslpath -u 'C:\...'` antes de pasar el flag.
|
||||
- **`taskkill` solo funciona en WSL con acceso a Windows tools**: si `tasklist.exe` y `taskkill.exe` no están en `$PATH` (instalación WSL sin interop habilitado), el paso 3 fallará. Verificar con `command -v tasklist.exe`.
|
||||
- **`powershell.exe` necesario para el lanzamiento**: `launch_cpp_app_windows` usa `Start-Process` de PowerShell. Si PowerShell no está en `$PATH`, el paso 4 fallará.
|
||||
- **`export_hub_icons` requiere Phosphor SVGs**: los SVGs deben existir en `sources/phosphor-core/assets/fill/`. Si no están clonados, las apps sin SVG se omiten (skip) sin abortar; el count final puede ser menor al esperado. Clonar con: `git clone https://github.com/phosphor-icons/core.git sources/phosphor-core`.
|
||||
- **Idempotente**: lanzable N veces. Si el hub no está corriendo, el paso 3 se salta y el paso 4 lo lanza igualmente. Si ya está corriendo, lo mata y lo relanza.
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# refresh_app_hub — Pipeline: regenera icons + manifest del App Hub y reinicia el proceso
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
source "$SCRIPT_DIR/../infra/is_cpp_app_running_windows.sh"
|
||||
source "$SCRIPT_DIR/../infra/launch_cpp_app_windows.sh"
|
||||
|
||||
PYTHON="${REGISTRY_ROOT}/python/.venv/bin/python3"
|
||||
HUB_APP="app_hub_launcher"
|
||||
|
||||
refresh_app_hub() {
|
||||
local hub_dir="/mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files"
|
||||
local size=64
|
||||
local no_restart=0
|
||||
local style="white_duotone"
|
||||
|
||||
# Parsear flags
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--hub-dir)
|
||||
hub_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--size)
|
||||
size="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-restart)
|
||||
no_restart=1
|
||||
shift
|
||||
;;
|
||||
--style)
|
||||
style="$2"
|
||||
shift 2
|
||||
;;
|
||||
-*)
|
||||
echo "refresh_app_hub: flag desconocido: $1" >&2
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
echo "refresh_app_hub: argumento inesperado: $1" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local icons_dir="${hub_dir}/icons"
|
||||
local manifest_path="${hub_dir}/hub_manifest.tsv"
|
||||
|
||||
# Paso 1: exportar PNGs de iconos
|
||||
echo "[1/4] Exporting PNG icons (size=${size}px) → ${icons_dir} ..."
|
||||
local icons_json
|
||||
icons_json=$(
|
||||
PYTHONPATH="${REGISTRY_ROOT}/python/functions" \
|
||||
FN_REGISTRY_ROOT="${REGISTRY_ROOT}" \
|
||||
"$PYTHON" \
|
||||
"${REGISTRY_ROOT}/python/functions/infra/export_hub_icons.py" \
|
||||
"$icons_dir" \
|
||||
--size "$size" \
|
||||
--registry-root "$REGISTRY_ROOT" \
|
||||
--style "$style"
|
||||
) || {
|
||||
echo "ERROR [1/4]: export_hub_icons falló" >&2
|
||||
return 1
|
||||
}
|
||||
local icon_count
|
||||
icon_count=$(echo "$icons_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])" 2>/dev/null || echo "?")
|
||||
echo "[1/4] PNG icons exported: ${icon_count}"
|
||||
|
||||
# Paso 2: exportar TSV manifest
|
||||
echo "[2/4] Exporting manifest → ${manifest_path} ..."
|
||||
local manifest_json
|
||||
manifest_json=$(
|
||||
PYTHONPATH="${REGISTRY_ROOT}/python/functions" \
|
||||
FN_REGISTRY_ROOT="${REGISTRY_ROOT}" \
|
||||
"$PYTHON" \
|
||||
"${REGISTRY_ROOT}/python/functions/infra/export_hub_manifest.py" \
|
||||
"$manifest_path" \
|
||||
--registry-root "$REGISTRY_ROOT"
|
||||
) || {
|
||||
echo "ERROR [2/4]: export_hub_manifest falló" >&2
|
||||
return 1
|
||||
}
|
||||
local manifest_count
|
||||
manifest_count=$(echo "$manifest_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])" 2>/dev/null || echo "?")
|
||||
echo "[2/4] Manifest exported: ${manifest_count} rows"
|
||||
|
||||
# Si --no-restart, terminar aqui
|
||||
if [[ $no_restart -eq 1 ]]; then
|
||||
echo "[3/4] Kill skipped (--no-restart)"
|
||||
echo "[4/4] Launch skipped (--no-restart)"
|
||||
echo "OK: app_hub refreshed (no-restart)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Paso 3: matar el hub si está corriendo
|
||||
local running=0
|
||||
if is_cpp_app_running_windows "$HUB_APP" >/dev/null 2>&1; then
|
||||
running=1
|
||||
fi
|
||||
|
||||
if [[ $running -eq 1 ]]; then
|
||||
echo "[3/4] Hub running → killing ${HUB_APP}.exe ..."
|
||||
taskkill.exe /IM "${HUB_APP}.exe" /F >/dev/null 2>&1 || {
|
||||
echo "ERROR [3/4]: taskkill falló para ${HUB_APP}.exe" >&2
|
||||
return 1
|
||||
}
|
||||
# Pequeña pausa para que Windows libere el handle antes del relanzamiento
|
||||
sleep 1
|
||||
echo "[3/4] Hub running → killed"
|
||||
else
|
||||
echo "[3/4] Hub not running → skip kill"
|
||||
fi
|
||||
|
||||
# Paso 4: relanzar el hub
|
||||
echo "[4/4] Launching ${HUB_APP} ..."
|
||||
if ! launch_cpp_app_windows "$HUB_APP"; then
|
||||
echo "ERROR [4/4]: launch_cpp_app_windows falló para '${HUB_APP}'" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "[4/4] Hub launched"
|
||||
|
||||
echo "OK: app_hub refreshed"
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente (fn run lo invoca como script)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
refresh_app_hub "$@"
|
||||
fi
|
||||
+255
-1
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
@@ -39,6 +40,8 @@ func cmdDoctor(args []string) {
|
||||
doctorArtefacts(r, jsonOut)
|
||||
case "services":
|
||||
doctorServices(r, jsonOut)
|
||||
case "services-spec":
|
||||
doctorServicesSpec(r, jsonOut)
|
||||
case "sync":
|
||||
doctorSync(r, jsonOut)
|
||||
case "uses-functions":
|
||||
@@ -59,6 +62,14 @@ func cmdDoctor(args []string) {
|
||||
} else {
|
||||
doctorCapabilities(r, jsonOut)
|
||||
}
|
||||
case "app-location":
|
||||
doctorAppLocation(r, jsonOut)
|
||||
case "modules":
|
||||
doctorModules(r, jsonOut)
|
||||
case "dod":
|
||||
doctorDod(r, jsonOut)
|
||||
case "e2e-coverage":
|
||||
doctorE2ECoverage(r, jsonOut)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
|
||||
doctorUsage()
|
||||
@@ -76,6 +87,7 @@ Subcommands:
|
||||
(none)|all Corre todos los checks
|
||||
artefacts Salud de apps y analyses (git, venv, app.md, upstream)
|
||||
services Estado de apps con tag 'service' (systemd + puerto)
|
||||
services-spec Audit del bloque service: en app.md de apps tag 'service' (issue 0105)
|
||||
sync Drift entre pc_locations BD y disco
|
||||
uses-functions Audit imports reales vs uses_functions del app.md
|
||||
unused Funciones del registry sin consumidores
|
||||
@@ -84,6 +96,10 @@ Subcommands:
|
||||
vaults Salud de vaults: directorio, layout, índice, staleness, drift
|
||||
copied-code Detecta cuerpos de funcion del registry copiados en apps sin import (issue 0085k)
|
||||
capabilities Drift entre docs/capabilities/INDEX.md, tags de funciones, y paginas <grupo>.md (issue 0086)
|
||||
app-location Detecta artefactos (apps/analysis) en carpetas de lenguaje (cpp/apps/, etc.) - issue 0096
|
||||
modules Drift entre uses_modules (app.md) y fn_module_<x> link calls (CMakeLists.txt) - issue 0097
|
||||
dod Audita bloque dod_evidence_schema en dev/issues/ y dev/flows/ (issue 0114)
|
||||
e2e-coverage Porcentaje de apps con e2e_checks declarado en su app.md (issue 0121b)
|
||||
|
||||
Flags:
|
||||
--json Salida JSON (para scripting/agentes)
|
||||
@@ -123,6 +139,11 @@ func doctorAll(root string, jsonOut bool) {
|
||||
} else {
|
||||
all["cpp_apps_error"] = err.Error()
|
||||
}
|
||||
if v, err := infra.AuditCppTableMigration(root); err == nil {
|
||||
all["cpp_table_migration"] = v
|
||||
} else {
|
||||
all["cpp_table_migration_error"] = err.Error()
|
||||
}
|
||||
if v, err := infra.AuditMlEnv(root); err == nil {
|
||||
all["ml"] = v
|
||||
} else {
|
||||
@@ -168,10 +189,21 @@ func doctorCppApps(root string, jsonOut bool) {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
tableAudits, err2 := infra.AuditCppTableMigration(root)
|
||||
if err2 != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: table migration audit failed: %v\n", err2)
|
||||
tableAudits = nil
|
||||
}
|
||||
|
||||
if jsonOut {
|
||||
emit(audits)
|
||||
emit(map[string]any{
|
||||
"conformance": audits,
|
||||
"table_migration": tableAudits,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Conformance section.
|
||||
bad := 0
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "STATUS\tAPP\tISSUES")
|
||||
@@ -187,6 +219,31 @@ func doctorCppApps(root string, jsonOut bool) {
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Printf("\n%d/%d C++ apps conform.\n", len(audits)-bad, len(audits))
|
||||
|
||||
// BeginTable migration section.
|
||||
if len(tableAudits) == 0 {
|
||||
return
|
||||
}
|
||||
hasMigrationNotes := false
|
||||
for _, t := range tableAudits {
|
||||
if t.Status != "clean" {
|
||||
hasMigrationNotes = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMigrationNotes {
|
||||
return
|
||||
}
|
||||
fmt.Println("\n--- BeginTable migration (issue 0081) ---")
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "STATUS\tAPP\tTABLES\tMESSAGE")
|
||||
for _, t := range tableAudits {
|
||||
if t.Status == "clean" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", strings.ToUpper(t.Status), t.AppID, t.BeginTableCount, t.Message)
|
||||
}
|
||||
tw.Flush()
|
||||
}
|
||||
|
||||
func doctorArtefacts(root string, jsonOut bool) {
|
||||
@@ -244,6 +301,58 @@ func doctorServices(root string, jsonOut bool) {
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func doctorServicesSpec(root string, jsonOut bool) {
|
||||
audits, err := infra.AuditServicesSpec(root)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if jsonOut {
|
||||
emit(audits)
|
||||
return
|
||||
}
|
||||
if len(audits) == 0 {
|
||||
fmt.Println("No services declared (no apps with tag 'service').")
|
||||
return
|
||||
}
|
||||
bad := 0
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "STATUS\tAPP\tRUNTIME\tPORT\tHEALTH\tUNIT\tTARGETS\tISSUES")
|
||||
for _, a := range audits {
|
||||
status := "OK"
|
||||
issues := "-"
|
||||
if !a.OK {
|
||||
status = "FAIL"
|
||||
issues = strings.Join(a.Issues, "; ")
|
||||
bad++
|
||||
}
|
||||
port := "-"
|
||||
if a.Port > 0 {
|
||||
port = fmt.Sprintf("%d", a.Port)
|
||||
}
|
||||
health := a.HealthPath
|
||||
if health == "" {
|
||||
health = "-"
|
||||
}
|
||||
unit := a.SystemdUnit
|
||||
if unit == "" {
|
||||
unit = "-"
|
||||
}
|
||||
targets := strings.Join(a.PCTargets, ",")
|
||||
if targets == "" {
|
||||
targets = "-"
|
||||
}
|
||||
runtime := a.Runtime
|
||||
if runtime == "" {
|
||||
runtime = "-"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
status, a.Name, runtime, port, health, unit, targets, issues)
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Printf("\n%d/%d services with complete service: block.\n", len(audits)-bad, len(audits))
|
||||
}
|
||||
|
||||
func doctorSync(root string, jsonOut bool) {
|
||||
drifts, err := infra.PcLocationsDrift(root, "")
|
||||
if err != nil {
|
||||
@@ -472,3 +581,148 @@ func doctorCopiedCode(root string, jsonOut bool) {
|
||||
w.Flush()
|
||||
fmt.Printf("\n%d suspected copy match(es).\n", len(entries))
|
||||
}
|
||||
|
||||
func doctorAppLocation(root string, jsonOut bool) {
|
||||
violations, err := infra.AuditAppLocation(root)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if jsonOut {
|
||||
emit(violations)
|
||||
return
|
||||
}
|
||||
if len(violations) == 0 {
|
||||
fmt.Println("OK: no artefacts under language-named folders.")
|
||||
return
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "KIND\tLANG\tPATH")
|
||||
for _, v := range violations {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", v.Kind, v.Lang, v.Path)
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Printf("\n%d violation(s): move artefact to apps/<name>/ or projects/<p>/apps/<name>/ (issue 0096).\n", len(violations))
|
||||
}
|
||||
|
||||
func doctorModules(root string, jsonOut bool) {
|
||||
checks, err := infra.AuditModulesDrift(root)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if jsonOut {
|
||||
emit(checks)
|
||||
return
|
||||
}
|
||||
|
||||
bad := 0
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "STATUS\tAPP\tDECLARED\tLINKED\tMISSING\tEXTRA")
|
||||
for _, c := range checks {
|
||||
status := "OK"
|
||||
if !c.OK {
|
||||
status = "DRIFT"
|
||||
bad++
|
||||
}
|
||||
decl := strings.Join(c.Declared, ",")
|
||||
if decl == "" {
|
||||
decl = "-"
|
||||
}
|
||||
link := strings.Join(c.Linked, ",")
|
||||
if link == "" {
|
||||
link = "-"
|
||||
}
|
||||
missing := strings.Join(c.MissingLinks, ",")
|
||||
if missing == "" {
|
||||
missing = "-"
|
||||
}
|
||||
extra := strings.Join(c.ExtraLinks, ",")
|
||||
if extra == "" {
|
||||
extra = "-"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", status, c.AppID, decl, link, missing, extra)
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Printf("\n%d/%d apps with module drift.\n", bad, len(checks))
|
||||
if bad > 0 {
|
||||
fmt.Println("Fix: align uses_modules in app.md with target_link_libraries(fn_module_*) in CMakeLists.txt.")
|
||||
}
|
||||
}
|
||||
|
||||
func doctorDod(root string, jsonOut bool) {
|
||||
issuesDir := filepath.Join(root, "dev", "issues")
|
||||
flowsDir := filepath.Join(root, "dev", "flows")
|
||||
report, err := infra.AuditDodSchema(issuesDir, flowsDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if jsonOut {
|
||||
emit(report)
|
||||
return
|
||||
}
|
||||
fmt.Println("=== DoD Schema Audit ===")
|
||||
fmt.Printf("files scanned: %d\n", report.TotalFiles)
|
||||
fmt.Printf("with schema: %d\n", report.FilesWithItems)
|
||||
fmt.Printf("total items: %d\n", report.TotalItems)
|
||||
fmt.Printf("invalid items: %d\n", report.InvalidItems)
|
||||
if report.InvalidItems == 0 {
|
||||
fmt.Println("\nAll DoD schemas valid.")
|
||||
return
|
||||
}
|
||||
fmt.Println()
|
||||
rel := func(p string) string {
|
||||
if r, err := filepath.Rel(root, p); err == nil {
|
||||
return r
|
||||
}
|
||||
return p
|
||||
}
|
||||
for _, f := range report.Files {
|
||||
if len(f.Errors) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range f.Errors {
|
||||
fmt.Printf("%s : %s\n", rel(f.Path), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doctorE2ECoverage(root string, jsonOut bool) {
|
||||
roots := []string{
|
||||
filepath.Join(root, "apps"),
|
||||
filepath.Join(root, "cpp", "apps"),
|
||||
filepath.Join(root, "projects"),
|
||||
}
|
||||
report, err := infra.AuditE2ECoverage(roots)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if jsonOut {
|
||||
emit(report)
|
||||
return
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "METRIC\tVALUE")
|
||||
fmt.Fprintf(w, "total\t%d\n", report.Total)
|
||||
fmt.Fprintf(w, "with_checks\t%d\n", report.WithChecks)
|
||||
fmt.Fprintf(w, "missing\t%d\n", len(report.Missing))
|
||||
fmt.Fprintf(w, "coverage_pct\t%.2f%%\n", report.CoveragePct)
|
||||
w.Flush()
|
||||
|
||||
if len(report.Missing) > 0 {
|
||||
fmt.Println("\nApps without e2e_checks:")
|
||||
rel := func(p string) string {
|
||||
if r, err := filepath.Rel(root, p); err == nil {
|
||||
return r
|
||||
}
|
||||
return p
|
||||
}
|
||||
for _, m := range report.Missing {
|
||||
fmt.Printf(" %s\n", rel(m))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-2
@@ -143,8 +143,8 @@ func cmdIndex() {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d unit_tests\n",
|
||||
result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.UnitTests)
|
||||
fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d modules, %d unit_tests\n",
|
||||
result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.Modules, result.UnitTests)
|
||||
for _, e := range result.ValidationErrors {
|
||||
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
|
||||
}
|
||||
@@ -420,10 +420,42 @@ func cmdShow(args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
m, errM := db.GetModule(id)
|
||||
if errM == nil {
|
||||
printModule(m)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "not found: %s\n", id)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func printModule(m *registry.Module) {
|
||||
fmt.Printf("ID: %s\n", m.ID)
|
||||
fmt.Printf("Name: %s\n", m.Name)
|
||||
fmt.Printf("Version: %s\n", m.Version)
|
||||
fmt.Printf("Lang: %s\n", m.Lang)
|
||||
fmt.Printf("Description: %s\n", m.Description)
|
||||
if len(m.Members) > 0 {
|
||||
fmt.Printf("Members: %s\n", strings.Join(m.Members, ", "))
|
||||
}
|
||||
if len(m.Tags) > 0 {
|
||||
fmt.Printf("Tags: %s\n", strings.Join(m.Tags, ", "))
|
||||
}
|
||||
if m.DirPath != "" {
|
||||
fmt.Printf("DirPath: %s\n", m.DirPath)
|
||||
}
|
||||
if m.RepoURL != "" {
|
||||
fmt.Printf("RepoURL: %s\n", m.RepoURL)
|
||||
}
|
||||
if m.Documentation != "" {
|
||||
fmt.Printf("\nDocumentation:\n%s\n", m.Documentation)
|
||||
}
|
||||
if m.Notes != "" {
|
||||
fmt.Printf("\nNotes:\n%s\n", m.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func printFunction(f *registry.Function) {
|
||||
fmt.Printf("ID: %s\n", f.ID)
|
||||
fmt.Printf("Name: %s\n", f.Name)
|
||||
@@ -540,6 +572,9 @@ func printApp(a *registry.App) {
|
||||
if len(a.UsesTypes) > 0 {
|
||||
fmt.Printf("Uses types: %s\n", strings.Join(a.UsesTypes, ", "))
|
||||
}
|
||||
if len(a.UsesModules) > 0 {
|
||||
fmt.Printf("Uses mods: %s\n", strings.Join(a.UsesModules, ", "))
|
||||
}
|
||||
if a.Notes != "" {
|
||||
fmt.Printf("\nNotes:\n%s\n", a.Notes)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type syncRequest struct {
|
||||
Analysis []registry.Analysis `json:"analysis"`
|
||||
Projects []registry.Project `json:"projects"`
|
||||
Vaults []registry.Vault `json:"vaults"`
|
||||
Modules []registry.Module `json:"modules"`
|
||||
Proposals []registry.Proposal `json:"proposals"`
|
||||
Locations []registry.PcLocation `json:"locations"`
|
||||
}
|
||||
@@ -37,6 +38,7 @@ type syncResponse struct {
|
||||
Analysis []registry.Analysis `json:"analysis"`
|
||||
Projects []registry.Project `json:"projects"`
|
||||
Vaults []registry.Vault `json:"vaults"`
|
||||
Modules []registry.Module `json:"modules"`
|
||||
Proposals []registry.Proposal `json:"proposals"`
|
||||
Locations []registry.PcLocation `json:"locations"`
|
||||
Stats struct {
|
||||
@@ -100,6 +102,7 @@ func syncPushPull() {
|
||||
analysis, _ := db.AllAnalysis()
|
||||
projects, _ := db.ListAllProjects()
|
||||
vaults, _ := db.AllVaults()
|
||||
modules, _ := db.AllModules()
|
||||
proposals, _ := db.AllProposals()
|
||||
|
||||
// 2. Scan local directories and build pc_locations
|
||||
@@ -112,6 +115,7 @@ func syncPushPull() {
|
||||
Analysis: analysis,
|
||||
Projects: projects,
|
||||
Vaults: vaults,
|
||||
Modules: modules,
|
||||
Proposals: proposals,
|
||||
Locations: locations,
|
||||
}
|
||||
@@ -203,6 +207,14 @@ func applySync(db *registry.DB, resp syncResponse) int {
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range resp.Modules {
|
||||
existing, err := db.GetModule(m.ID)
|
||||
if err != nil || m.UpdatedAt.After(existing.UpdatedAt) {
|
||||
db.InsertModule(&m)
|
||||
imported++
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range resp.Proposals {
|
||||
existing, err := db.GetProposal(p.ID)
|
||||
if err != nil || p.UpdatedAt.After(existing.UpdatedAt) {
|
||||
@@ -329,6 +341,7 @@ func syncStatus() {
|
||||
analysis, _ := db.AllAnalysis()
|
||||
projects, _ := db.ListAllProjects()
|
||||
vaults, _ := db.AllVaults()
|
||||
modules, _ := db.AllModules()
|
||||
proposals, _ := db.AllProposals()
|
||||
locs, _ := db.ListAllPcLocations()
|
||||
|
||||
@@ -337,6 +350,7 @@ func syncStatus() {
|
||||
fmt.Printf(" analysis: %d\n", len(analysis))
|
||||
fmt.Printf(" projects: %d\n", len(projects))
|
||||
fmt.Printf(" vaults: %d\n", len(vaults))
|
||||
fmt.Printf(" modules: %d\n", len(modules))
|
||||
fmt.Printf(" proposals: %d\n", len(proposals))
|
||||
fmt.Printf(" locations: %d\n", len(locs))
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"fn-registry/functions/browser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", 9222, "CDP debug port")
|
||||
headless := flag.Bool("headless", false, "headless mode")
|
||||
chromePath := flag.String("chrome-path", "", "explicit chrome.exe path (optional)")
|
||||
userDataDir := flag.String("user-data-dir", "", "user-data-dir (optional; WSL2 auto-translates)")
|
||||
flag.Parse()
|
||||
|
||||
pid, err := browser.ChromeLaunch(browser.ChromeLaunchOpts{
|
||||
Port: *port,
|
||||
Headless: *headless,
|
||||
ChromePath: *chromePath,
|
||||
UserDataDir: *userDataDir,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chrome_launch failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("OK pid=%d port=%d\n", pid, *port)
|
||||
}
|
||||
+187
-31
@@ -1,5 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(fn_registry_cpp LANGUAGES C CXX)
|
||||
if(WIN32)
|
||||
project(fn_registry_cpp LANGUAGES C CXX RC)
|
||||
else()
|
||||
project(fn_registry_cpp LANGUAGES C CXX)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -236,10 +240,75 @@ endif()
|
||||
set(FN_CPP_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "fn_registry cpp root")
|
||||
|
||||
function(add_imgui_app target)
|
||||
add_executable(${target} ${ARGN})
|
||||
# Windows icon: si la app tiene <app_dir>/appicon.ico, generamos un .rc
|
||||
# apuntando a ese .ico y lo anadimos como fuente. mingw-w64 windres
|
||||
# (CMAKE_RC_COMPILER en la toolchain) lo enlaza en el .exe.
|
||||
set(_extra_sources "")
|
||||
if(WIN32 AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico)
|
||||
set(_rc_file ${CMAKE_CURRENT_BINARY_DIR}/${target}_appicon.rc)
|
||||
# Forward slashes para que windres no se confunda con escapes.
|
||||
file(TO_CMAKE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico _ico_path)
|
||||
# Numeric ID 101 = FN_APP_ICON_ID (ver cpp/framework/app_base.cpp).
|
||||
# Usamos ID numerico (no string "IDI_ICON1") para que LoadImageW
|
||||
# pueda recuperarlo en runtime y attacharlo al HWND (WM_SETICON).
|
||||
file(WRITE ${_rc_file} "101 ICON \"${_ico_path}\"\n")
|
||||
list(APPEND _extra_sources ${_rc_file})
|
||||
endif()
|
||||
|
||||
# Modules manifest (issue 0097): siempre generamos <target>_modules_generated.cpp.
|
||||
# Si la app tiene app.md con uses_modules, el .cpp resultante define
|
||||
# fn::app_modules_array[] con sus modulos. Si no, genera un stub vacio
|
||||
# (apps sin app.md no rompen el linkage de framework's app_about).
|
||||
set(_modules_gen ${CMAKE_CURRENT_BINARY_DIR}/${target}_modules_generated.cpp)
|
||||
set(_codegen_script ${FN_CPP_ROOT_DIR}/../python/functions/infra/codegen_app_modules.py)
|
||||
set(_modules_root ${FN_CPP_ROOT_DIR}/../modules)
|
||||
set(_app_md ${CMAKE_CURRENT_SOURCE_DIR}/app.md)
|
||||
if(NOT EXISTS ${_app_md})
|
||||
# No app.md: emit empty stub directamente (sin invocar Python).
|
||||
file(WRITE ${_modules_gen}
|
||||
"// Auto-generated stub (no app.md).
|
||||
#include \"app_modules.h\"
|
||||
namespace fn {
|
||||
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
|
||||
const unsigned long app_modules_count = 0;
|
||||
}
|
||||
")
|
||||
else()
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_FOUND AND EXISTS ${_codegen_script})
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} ${_codegen_script}
|
||||
--app-md ${_app_md}
|
||||
--modules-root ${_modules_root}
|
||||
--app-name ${target}
|
||||
--out ${_modules_gen}
|
||||
RESULT_VARIABLE _codegen_rc
|
||||
OUTPUT_VARIABLE _codegen_out
|
||||
ERROR_VARIABLE _codegen_err
|
||||
)
|
||||
if(NOT _codegen_rc EQUAL 0 AND NOT _codegen_rc EQUAL 2)
|
||||
message(WARNING "codegen_app_modules failed for ${target}: ${_codegen_err}")
|
||||
endif()
|
||||
endif()
|
||||
# Si python falla o el script no esta, emit stub vacio.
|
||||
if(NOT EXISTS ${_modules_gen})
|
||||
file(WRITE ${_modules_gen}
|
||||
"// Auto-generated stub (codegen unavailable).
|
||||
#include \"app_modules.h\"
|
||||
namespace fn {
|
||||
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
|
||||
const unsigned long app_modules_count = 0;
|
||||
}
|
||||
")
|
||||
endif()
|
||||
endif()
|
||||
list(APPEND _extra_sources ${_modules_gen})
|
||||
|
||||
add_executable(${target} ${ARGN} ${_extra_sources})
|
||||
target_link_libraries(${target} PRIVATE fn_framework)
|
||||
target_include_directories(${target} PRIVATE
|
||||
${FN_CPP_ROOT_DIR}/functions
|
||||
${FN_CPP_ROOT_DIR}/framework
|
||||
)
|
||||
# Convencion de layout (cpp_apps.md §7):
|
||||
# <exe_dir>/<app>.exe + <app>.dll (binario + DLLs Windows convention)
|
||||
@@ -274,14 +343,29 @@ endfunction()
|
||||
# Functions are compiled as part of apps that use them via add_imgui_app.
|
||||
# Each function is a .h/.cpp pair included by the app's CMakeLists.txt.
|
||||
|
||||
# --- Demo app ---
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt)
|
||||
add_subdirectory(apps/chart_demo)
|
||||
# --- fn_module_data_table (issue 0097 modules) ---
|
||||
# Static lib defined in modules/data_table/CMakeLists.txt. Replaces former
|
||||
# fn_module_data_table target. Apps opt-in via:
|
||||
# target_link_libraries(<app> PRIVATE fn_module_data_table)
|
||||
# Lua is a hard dep — only build the module when the vendored lua tree exists.
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/../modules/data_table ${CMAKE_BINARY_DIR}/modules/data_table)
|
||||
endif()
|
||||
|
||||
# --- Shaders Lab ---
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt)
|
||||
add_subdirectory(apps/shaders_lab)
|
||||
# --- Demo app (lives in apps/, issue 0096 standardization) ---
|
||||
if(NOT DEFINED _CHART_DEMO_DIR)
|
||||
set(_CHART_DEMO_DIR ${CMAKE_SOURCE_DIR}/../apps/chart_demo)
|
||||
endif()
|
||||
if(EXISTS ${_CHART_DEMO_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_CHART_DEMO_DIR} ${CMAKE_BINARY_DIR}/apps/chart_demo)
|
||||
endif()
|
||||
|
||||
# --- Shaders Lab (lives in apps/) ---
|
||||
if(NOT DEFINED _SHADERS_LAB_DIR)
|
||||
set(_SHADERS_LAB_DIR ${CMAKE_SOURCE_DIR}/../apps/shaders_lab)
|
||||
endif()
|
||||
if(EXISTS ${_SHADERS_LAB_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_SHADERS_LAB_DIR} ${CMAKE_BINARY_DIR}/apps/shaders_lab)
|
||||
endif()
|
||||
|
||||
# --- Lua 5.4 vendored (para playground tables / DSL formulas) ---
|
||||
@@ -289,30 +373,39 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
|
||||
add_subdirectory(vendor/lua)
|
||||
endif()
|
||||
|
||||
# --- Primitives Gallery (catalogo visual de primitivos core/viz/gfx) ---
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt)
|
||||
add_subdirectory(apps/primitives_gallery)
|
||||
# --- Primitives Gallery (lives in apps/) ---
|
||||
if(NOT DEFINED _PG_DIR)
|
||||
set(_PG_DIR ${CMAKE_SOURCE_DIR}/../apps/primitives_gallery)
|
||||
endif()
|
||||
if(EXISTS ${_PG_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_PG_DIR} ${CMAKE_BINARY_DIR}/apps/primitives_gallery)
|
||||
endif()
|
||||
|
||||
# --- Tables playground (vive dentro de primitives_gallery/playground/tables/) ---
|
||||
# No es un app del registry; sirve para iterar mejoras sobre table_view_cpp_viz
|
||||
# antes de promover una API v2 y migrar las apps C++ que hoy usan ImGui::BeginTable raw.
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/playground/tables/CMakeLists.txt)
|
||||
add_subdirectory(apps/primitives_gallery/playground/tables)
|
||||
# --- Tables playground DEPRECATED (issue 0108) ---
|
||||
# Sustituido por apps/tables_qa. El playground legacy queda solo como historia
|
||||
# del split data_table 0107c. NO se builda mas — su self_test (430 checks
|
||||
# contra logica legacy) ya esta cubierto por:
|
||||
# - cpp/tests/ (Catch2 unit tests de la logica pura del registry)
|
||||
# - apps/tables_qa/ (testbed del modulo data_table v2.0.0+)
|
||||
# Para revivirlo (temporal, debugging): descomentar el bloque if(EXISTS ...).
|
||||
# if(EXISTS ${_PG_DIR}/playground/tables/CMakeLists.txt)
|
||||
# add_subdirectory(${_PG_DIR}/playground/tables ${CMAKE_BINARY_DIR}/apps/primitives_gallery/playground/tables)
|
||||
# endif()
|
||||
|
||||
# --- text_editor + file_watcher smoke test (lives in apps/) ---
|
||||
if(NOT DEFINED _TES_DIR)
|
||||
set(_TES_DIR ${CMAKE_SOURCE_DIR}/../apps/text_editor_smoke)
|
||||
endif()
|
||||
if(EXISTS ${_TES_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_TES_DIR} ${CMAKE_BINARY_DIR}/apps/text_editor_smoke)
|
||||
endif()
|
||||
|
||||
# --- text_editor + file_watcher smoke test (issue 0025) ---
|
||||
# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan.
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt)
|
||||
add_subdirectory(apps/text_editor_smoke)
|
||||
# --- AltSnap viewport-jitter regression test (lives in apps/) ---
|
||||
if(NOT DEFINED _AJT_DIR)
|
||||
set(_AJT_DIR ${CMAKE_SOURCE_DIR}/../apps/altsnap_jitter_test)
|
||||
endif()
|
||||
|
||||
# --- AltSnap viewport-jitter regression test ---
|
||||
# Headless harness que conduce glfwSetWindowPos cada frame y verifica que
|
||||
# ImGui viewport->Pos sigue al OS dentro de 1px. Sin la patch del framework
|
||||
# (callback GLFW + per-frame sync) este test falla exit=1.
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/altsnap_jitter_test/CMakeLists.txt)
|
||||
add_subdirectory(apps/altsnap_jitter_test)
|
||||
if(EXISTS ${_AJT_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_AJT_DIR} ${CMAKE_BINARY_DIR}/apps/altsnap_jitter_test)
|
||||
endif()
|
||||
|
||||
# --- gamedev stack (SDL3 + sokol_gfx + miniaudio, issue 0072) ---
|
||||
@@ -328,11 +421,17 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sdl3/CMakeLists.txt
|
||||
set(SDL_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(vendor/sdl3 EXCLUDE_FROM_ALL)
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/engine_smoke/CMakeLists.txt)
|
||||
add_subdirectory(apps/engine_smoke)
|
||||
if(NOT DEFINED _ES_DIR)
|
||||
set(_ES_DIR ${CMAKE_SOURCE_DIR}/../apps/engine_smoke)
|
||||
endif()
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/runtime_test/CMakeLists.txt)
|
||||
add_subdirectory(apps/runtime_test)
|
||||
if(EXISTS ${_ES_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_ES_DIR} ${CMAKE_BINARY_DIR}/apps/engine_smoke)
|
||||
endif()
|
||||
if(NOT DEFINED _RT_DIR)
|
||||
set(_RT_DIR ${CMAKE_SOURCE_DIR}/../apps/runtime_test)
|
||||
endif()
|
||||
if(EXISTS ${_RT_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_RT_DIR} ${CMAKE_BINARY_DIR}/apps/runtime_test)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -379,3 +478,60 @@ if(BUILD_TESTING)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
|
||||
# --- dag_engine_ui (lives in apps/, issue 0096) ---
|
||||
if(NOT DEFINED _DAG_UI_DIR)
|
||||
set(_DAG_UI_DIR ${CMAKE_SOURCE_DIR}/../apps/dag_engine_ui)
|
||||
endif()
|
||||
if(EXISTS ${_DAG_UI_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_DAG_UI_DIR} ${CMAKE_BINARY_DIR}/apps/dag_engine_ui)
|
||||
endif()
|
||||
|
||||
# --- data_factory (lives in apps/, issue 0096) ---
|
||||
set(_DATA_FACTORY_DIR ${CMAKE_SOURCE_DIR}/../apps/data_factory)
|
||||
if(EXISTS ${_DATA_FACTORY_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_DATA_FACTORY_DIR} ${CMAKE_BINARY_DIR}/apps/data_factory)
|
||||
endif()
|
||||
|
||||
# --- app_hub_launcher (lives in apps/, issue 0096) ---
|
||||
set(_APP_HUB_LAUNCHER_DIR ${CMAKE_SOURCE_DIR}/../apps/app_hub_launcher)
|
||||
if(EXISTS ${_APP_HUB_LAUNCHER_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_APP_HUB_LAUNCHER_DIR} ${CMAKE_BINARY_DIR}/apps/app_hub_launcher)
|
||||
endif()
|
||||
|
||||
# --- services_monitor (lives in apps/, issue 0096) ---
|
||||
set(_SERVICES_MONITOR_DIR ${CMAKE_SOURCE_DIR}/../apps/services_monitor)
|
||||
if(EXISTS ${_SERVICES_MONITOR_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_SERVICES_MONITOR_DIR} ${CMAKE_BINARY_DIR}/apps/services_monitor)
|
||||
endif()
|
||||
|
||||
# --- app_gestion (lives in apps/, issue 0096) ---
|
||||
set(_APP_GESTION_DIR ${CMAKE_SOURCE_DIR}/../apps/app_gestion)
|
||||
if(EXISTS ${_APP_GESTION_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_APP_GESTION_DIR} ${CMAKE_BINARY_DIR}/apps/app_gestion)
|
||||
endif()
|
||||
|
||||
# --- skill_tree (lives in apps/, issue 0096) ---
|
||||
set(_SKILL_TREE_DIR ${CMAKE_SOURCE_DIR}/../apps/skill_tree)
|
||||
if(EXISTS ${_SKILL_TREE_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_SKILL_TREE_DIR} ${CMAKE_BINARY_DIR}/apps/skill_tree)
|
||||
endif()
|
||||
|
||||
# --- tables_qa (lives in apps/, issue 0096) ---
|
||||
set(_TABLES_QA_DIR ${CMAKE_SOURCE_DIR}/../apps/tables_qa)
|
||||
if(EXISTS ${_TABLES_QA_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_TABLES_QA_DIR} ${CMAKE_BINARY_DIR}/apps/tables_qa)
|
||||
endif()
|
||||
|
||||
# --- process_explorer (lives in apps/, issue 0096) ---
|
||||
set(_PROCESS_EXPLORER_DIR ${CMAKE_SOURCE_DIR}/../apps/process_explorer)
|
||||
if(EXISTS ${_PROCESS_EXPLORER_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_PROCESS_EXPLORER_DIR} ${CMAKE_BINARY_DIR}/apps/process_explorer)
|
||||
endif()
|
||||
|
||||
# --- agents_dashboard (lives in projects/element_agents/apps/) ---
|
||||
set(_AGENTS_DASHBOARD_DIR ${CMAKE_SOURCE_DIR}/../projects/element_agents/apps/agents_dashboard)
|
||||
if(EXISTS ${_AGENTS_DASHBOARD_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_AGENTS_DASHBOARD_DIR} ${CMAKE_BINARY_DIR}/apps/agents_dashboard)
|
||||
endif()
|
||||
|
||||
Submodule cpp/apps/altsnap_jitter_test deleted from 6e52b658a3
Submodule
+1
Submodule cpp/apps/chart_demo added at 026f514bb7
@@ -1,22 +0,0 @@
|
||||
add_imgui_app(chart_demo
|
||||
main.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
|
||||
# fps_overlay vive en fn_framework
|
||||
)
|
||||
|
||||
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
|
||||
if(FN_BUILD_TESTS)
|
||||
add_imgui_app(chart_demo_tests
|
||||
main.cpp
|
||||
tests/chart_demo_tests.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
|
||||
)
|
||||
# Excludes int main() from main.cpp so the test target provides its own.
|
||||
target_compile_definitions(chart_demo_tests PRIVATE FN_TEST_BUILD)
|
||||
endif()
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: chart_demo
|
||||
lang: cpp
|
||||
domain: viz
|
||||
description: "Demo ImGui de primitivos viz del registry: line_plot, scatter_plot, bar_chart, heatmap. Cada chart en su propia tab del TabBar. Usado como showcase y como build gate de las funciones viz/."
|
||||
tags: [imgui, demo, charts, viz, showcase]
|
||||
uses_functions:
|
||||
- line_plot_cpp_viz
|
||||
- scatter_plot_cpp_viz
|
||||
- bar_chart_cpp_viz
|
||||
- heatmap_cpp_viz
|
||||
# logger, app_menubar viven en fn_framework — no se listan aqui
|
||||
uses_types: []
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
dir_path: "cpp/apps/chart_demo"
|
||||
repo_url: ""
|
||||
---
|
||||
|
||||
## Que hace
|
||||
|
||||
App de una sola ventana con cuatro tabs (Line / Scatter / Bar / Heatmap) que
|
||||
renderiza datos sinteticos para mostrar el aspecto y la API de los primitivos
|
||||
viz del registry. Sirve como:
|
||||
|
||||
- **Showcase visual** de las funciones viz existentes — al añadir una nueva
|
||||
primitiva, anadir su tab aqui es la forma natural de probar el binding.
|
||||
- **Build gate**: si una de las funciones rompe API, esta app deja de
|
||||
compilar y lo cazamos sin tener que tocar `registry_dashboard` o
|
||||
`graph_explorer`.
|
||||
|
||||
## Estructura
|
||||
|
||||
`main.cpp` (~93 lineas):
|
||||
|
||||
- `init_data()` — genera arrays sinteticos una vez (estado modulo).
|
||||
- `render()` — DockSpaceOverViewport + TabBar con 4 tabs, cada una invoca
|
||||
un primitivo del registry.
|
||||
- `main()` → `fn::run_app(...)` con AppConfig estandar (titulo, tamaño,
|
||||
about, log).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target chart_demo
|
||||
|
||||
# Windows (cross-compile)
|
||||
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
|
||||
&& cmake --build build/windows --target chart_demo
|
||||
```
|
||||
|
||||
## Decisiones
|
||||
|
||||
- `viewports = true` (default de `fn::run_app`): las ventanas se pueden
|
||||
arrastrar fuera del main window.
|
||||
- `init_gl_loader = false`: solo usa ImGui/ImPlot, sin gl* directo.
|
||||
- Sin persistencia propia (no abre BD).
|
||||
- `log: file_path = "chart_demo.log"` con nivel Debug — el `init_data`
|
||||
emite info+debug para verificar que el logger funciona.
|
||||
@@ -1,89 +0,0 @@
|
||||
#include "app_base.h"
|
||||
#include "imgui.h"
|
||||
#include "implot.h"
|
||||
|
||||
#include "viz/line_plot.h"
|
||||
#include "viz/scatter_plot.h"
|
||||
#include "viz/bar_chart.h"
|
||||
#include "viz/heatmap.h"
|
||||
#include "core/app_menubar.h"
|
||||
#include "core/logger.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
// Generate sample data
|
||||
static constexpr int N = 500;
|
||||
static float xs[N], ys_sin[N], ys_cos[N];
|
||||
static float scatter_x[200], scatter_y[200];
|
||||
static const char* bar_labels[] = {"Go", "Python", "Bash", "TypeScript", "C++"};
|
||||
static float bar_values[] = {201.0f, 202.0f, 38.0f, 80.0f, 5.0f};
|
||||
static float heat_data[10 * 10];
|
||||
|
||||
static bool data_initialized = false;
|
||||
|
||||
static void init_data() {
|
||||
if (data_initialized) return;
|
||||
fn_log::log_info("init_data: generando %d puntos sin/cos, 200 scatter, 10x10 heatmap", N);
|
||||
for (int i = 0; i < N; i++) {
|
||||
xs[i] = static_cast<float>(i) * 0.02f;
|
||||
ys_sin[i] = sinf(xs[i]);
|
||||
ys_cos[i] = cosf(xs[i]);
|
||||
}
|
||||
for (int i = 0; i < 200; i++) {
|
||||
scatter_x[i] = static_cast<float>(rand()) / RAND_MAX * 10.0f;
|
||||
scatter_y[i] = scatter_x[i] * 0.5f + (static_cast<float>(rand()) / RAND_MAX - 0.5f) * 3.0f;
|
||||
}
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int r = i / 10, c = i % 10;
|
||||
heat_data[i] = sinf(r * 0.5f) * cosf(c * 0.5f);
|
||||
}
|
||||
data_initialized = true;
|
||||
fn_log::log_debug("init_data: ok");
|
||||
}
|
||||
|
||||
void render() {
|
||||
init_data();
|
||||
|
||||
if (ImGui::Begin("fn_registry — Chart Demo")) {
|
||||
if (ImGui::BeginTabBar("##charts")) {
|
||||
if (ImGui::BeginTabItem("Line Plot")) {
|
||||
ImGui::Text("sin(x) — %d points", N);
|
||||
line_plot("Sine Wave", xs, ys_sin, N);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Scatter Plot")) {
|
||||
ImGui::Text("y = 0.5x + noise — 200 points");
|
||||
scatter_plot("Scatter Data", scatter_x, scatter_y, 200);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Bar Chart")) {
|
||||
ImGui::Text("Functions per language in fn_registry");
|
||||
bar_chart("Registry Languages", bar_labels, bar_values, 5);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Heatmap")) {
|
||||
ImGui::Text("sin(r) * cos(c) — 10x10 matrix");
|
||||
heatmap("Correlation Matrix", heat_data, 10, 10, -1.0f, 1.0f);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
#ifndef FN_TEST_BUILD
|
||||
int main() {
|
||||
return fn::run_app({
|
||||
.title = "fn_registry — Chart Demo",
|
||||
.width = 1400,
|
||||
.height = 900,
|
||||
.about = {.name = "chart demo",
|
||||
.version = "0.2.0",
|
||||
.description = "Demo de primitivos viz: line, scatter, bar, heatmap. AppConfig estandar + multi-viewport."},
|
||||
.log = {.file_path = "chart_demo.log",
|
||||
.level = static_cast<int>(fn_log::Level::Debug)}
|
||||
}, render);
|
||||
}
|
||||
#endif
|
||||
@@ -1,41 +0,0 @@
|
||||
// E2E tests for chart_demo — Dear ImGui Test Engine.
|
||||
// Built only when -DFN_BUILD_TESTS=ON. The same main.cpp from chart_demo is
|
||||
// compiled here with FN_TEST_BUILD defined so its int main() is excluded and
|
||||
// only render() is reused.
|
||||
|
||||
#include "app_base.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_te_engine.h"
|
||||
#include "imgui_te_context.h"
|
||||
|
||||
void render(); // defined in chart_demo/main.cpp
|
||||
|
||||
static void register_tests(ImGuiTestEngine* e) {
|
||||
ImGuiTest* t = nullptr;
|
||||
|
||||
// Smoke test: the main window appears and is non-empty.
|
||||
t = IM_REGISTER_TEST(e, "chart_demo", "smoke_window_visible");
|
||||
t->TestFunc = [](ImGuiTestContext* ctx) {
|
||||
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo"); // em-dash
|
||||
IM_CHECK(ctx->WindowInfo("").ID != 0);
|
||||
};
|
||||
|
||||
// Cycle through all four tabs. Test engine fails the test if any tab item
|
||||
// is not found or cannot be activated — that is our implicit assertion.
|
||||
t = IM_REGISTER_TEST(e, "chart_demo", "tabs_cycle_all");
|
||||
t->TestFunc = [](ImGuiTestContext* ctx) {
|
||||
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo");
|
||||
ctx->ItemClick("##charts/Line Plot");
|
||||
ctx->ItemClick("##charts/Scatter Plot");
|
||||
ctx->ItemClick("##charts/Bar Chart");
|
||||
ctx->ItemClick("##charts/Heatmap");
|
||||
};
|
||||
}
|
||||
|
||||
int main() {
|
||||
fn::AppConfig cfg{};
|
||||
cfg.title = "chart_demo_tests";
|
||||
cfg.width = 1280;
|
||||
cfg.height = 800;
|
||||
return fn::run_app_test(cfg, render, register_tests);
|
||||
}
|
||||
Submodule cpp/apps/engine_smoke deleted from bed33856e7
@@ -1,110 +0,0 @@
|
||||
add_imgui_app(primitives_gallery
|
||||
main.cpp
|
||||
capture.cpp
|
||||
demo.cpp
|
||||
demos_core.cpp
|
||||
demos_viz.cpp
|
||||
demos_graph.cpp
|
||||
demos_graph_styles.cpp
|
||||
demos_gfx.cpp
|
||||
demos_3d.cpp
|
||||
demos_text_editor.cpp
|
||||
demos_gl_texture.cpp
|
||||
demos_extras.cpp
|
||||
demos_mesh.cpp
|
||||
# animation primitives (issue 0031)
|
||||
demos_animation.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/tween_curves.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/bezier_editor.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
|
||||
demos_sql.cpp
|
||||
demos_scientific.cpp
|
||||
# text_editor + file_watcher (issue 0025) + file_poll_diff pure (issue 0045)
|
||||
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
|
||||
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
|
||||
# sql_workbench (issue 0032) + sql_parse pure (issue 0045)
|
||||
${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.cpp
|
||||
# Core primitives demoed (tokens vive en fn_framework)
|
||||
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/icon_button.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/toolbar.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/select.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/process_state_machine.cpp
|
||||
# Viz primitives demoed
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/pie_chart.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/histogram.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/candlestick.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/gauge.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
|
||||
# 3D viz primitives (issue 0028, ImPlot3D)
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/surface_plot_3d.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/scatter_3d.cpp
|
||||
# Scientific viz (issue 0034)
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/treemap.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/sankey.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/chord.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/contour.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/voronoi.cpp
|
||||
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_icons.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout_gpu.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_layouts.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport_selection.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels_select.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp
|
||||
# GL loader (Linux no-op, Windows wglGetProcAddress)
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
|
||||
# Shader stack (shader_canvas demo)
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
|
||||
# gl_texture_load (issue 0026) + stb_image
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_texture_load.cpp
|
||||
${CMAKE_SOURCE_DIR}/vendor/stb/stb_image_impl.cpp
|
||||
# mesh_viewer stack (issue 0029)
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_obj_load.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp
|
||||
)
|
||||
target_include_directories(primitives_gallery PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
|
||||
${CMAKE_SOURCE_DIR}/vendor/stb
|
||||
)
|
||||
|
||||
# SQLite (sql_workbench) — alias provisto por cpp/CMakeLists.txt:
|
||||
# system on Linux, vendored amalgamation on Windows cross-compile.
|
||||
target_link_libraries(primitives_gallery PRIVATE SQLite::SQLite3)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(primitives_gallery PRIVATE opengl32)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
endif()
|
||||
@@ -1,159 +0,0 @@
|
||||
# primitives_gallery
|
||||
|
||||
Catalogo visual interactivo de los primitivos UI del registry (`cpp/functions/core` y `cpp/functions/viz`). Un solo ejecutable con sidebar izquierdo + panel derecho que renderiza la demo del primitivo seleccionado con todas sus variantes y un snippet de codigo.
|
||||
|
||||
## Rol
|
||||
|
||||
| Funcion | Como lo cumple |
|
||||
|---|---|
|
||||
| Smoke test visual | Abrir la gallery tras un cambio en tokens / componentes; si algo se ve raro, lo cazas en segundos. |
|
||||
| Documentacion viva | Cada demo muestra el componente trabajando + el snippet exacto. Mas rapido que leer los `.md`. |
|
||||
| Build gate | Esta en el CMake principal (`cpp/CMakeLists.txt`). Si un primitivo rompe API, la gallery no compila => CI rojo. |
|
||||
| Sandbox de prototipos | Datos sinteticos, sin backend; ideal para iterar un primitivo nuevo sin tocar el dashboard. |
|
||||
|
||||
## Build & run
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
cmake --build cpp/build/linux --target primitives_gallery -j$(nproc)
|
||||
./cpp/build/linux/apps/primitives_gallery/primitives_gallery
|
||||
|
||||
# Windows (cross-compile)
|
||||
cmake --build cpp/build/windows --target primitives_gallery -j$(nproc)
|
||||
# binario: cpp/build/windows/apps/primitives_gallery/primitives_gallery.exe
|
||||
```
|
||||
|
||||
No se conecta a `sqlite_api` ni a ningun backend. Datos sinteticos generados in-memory.
|
||||
|
||||
## Demos disponibles
|
||||
|
||||
### Core
|
||||
|
||||
| Demo | Primitivo | Que muestra |
|
||||
|---|---|---|
|
||||
| button | `button_cpp_core` | 4 variantes x 3 sizes |
|
||||
| icon_button | `icon_button_cpp_core` | Glyphs comunes con tooltip |
|
||||
| toolbar | `toolbar_cpp_core` | Dos grupos con separador vertical |
|
||||
| modal_dialog | `modal_dialog_cpp_core` | Boton que abre modal con form |
|
||||
| text_input | `text_input_cpp_core` | 3 inputs con placeholder |
|
||||
| select | `select_cpp_core` | Dropdown con y sin `(none)` |
|
||||
| toast + inbox | `toast_cpp_core` (v1.1) | 4 botones que disparan toasts + campana con badge |
|
||||
| tree_view | `tree_view_cpp_core` | Arbol fake de proyectos -> apps |
|
||||
| badge | `badge_cpp_core` | 6 variantes semanticas |
|
||||
| empty_state | `empty_state_cpp_core` | Lista vacia con icono + cta |
|
||||
| page_header | `page_header_cpp_core` | Header con toolbar a la derecha |
|
||||
| dashboard_panel | `dashboard_panel_cpp_core` | Panel con titulo y borde |
|
||||
| kpi_card | `kpi_card_cpp_viz` (v1.2) | Grid 1x4 con sparklines y delta |
|
||||
|
||||
### Viz
|
||||
|
||||
| Demo | Primitivo | Que muestra |
|
||||
|---|---|---|
|
||||
| bar_chart | `bar_chart_cpp_viz` (v1.2) | Labels que caben + labels rotados 45 |
|
||||
| pie_chart | `pie_chart_cpp_viz` (v1.1) | Pie + donut con tooltip por slice |
|
||||
| line_plot | `line_plot_cpp_viz` (v1.1) | Serie sintetica `sin(t) + ruido` |
|
||||
| scatter_plot | `scatter_plot_cpp_viz` (v1.1) | 120 puntos con correlacion |
|
||||
| histogram | `histogram_cpp_viz` (v1.1) | 300 muestras gaussianas |
|
||||
| sparkline | `sparkline_cpp_viz` | Trending up / down / flat |
|
||||
| graph_viewport | `graph_viewport_cpp_viz` | **Ver seccion abajo** |
|
||||
|
||||
## Demo `graph_viewport` (en detalle)
|
||||
|
||||
Pipeline completo de visualizacion de grafos con instanced GPU rendering:
|
||||
- `graph_renderer_cpp_viz` (1 draw call para todos los nodos via `glDrawArraysInstanced`)
|
||||
- `graph_force_layout_cpp_viz` (Barnes-Hut, paso de simulacion por frame)
|
||||
- `graph_spatial_hash_cpp_core` (hit-testing O(1) bajo el cursor)
|
||||
- `graph_viewport_cpp_viz` (widget que orquesta los anteriores con pan/zoom/select)
|
||||
|
||||
### Controles
|
||||
|
||||
| Control | Rango | Efecto |
|
||||
|---|---|---|
|
||||
| `Nodes` | 100 – 20 000 | Numero de nodos a generar |
|
||||
| `Clusters` | 2 – 16 | Numero de comunidades (cada una con su color) |
|
||||
| `Repulsion` | 100 – 20 000 | Fuerza repulsiva entre todos los nodos. Mas alto => grafo mas extendido y energia mayor. |
|
||||
| `Attraction` | 0.001 – 0.5 | Constante del muelle de las aristas. Mas alto => clusters mas compactos. |
|
||||
| `Gravity` | 0.0 – 0.05 | Tiron hacia (0,0). Util para evitar drift cuando subes mucho la repulsion. |
|
||||
| `Regenerate` | boton | Regenera el grafo con los valores actuales de Nodes/Clusters. |
|
||||
| `Pause / Resume layout` | boton | Para o reanuda la simulacion force-directed. |
|
||||
| `Fit view` | boton | Encuadra la camara al bounding box del grafo con 10% de padding. |
|
||||
|
||||
Los tres sliders de fuerzas se leen cada frame y se inyectan en `ForceLayoutConfig`, asi que cambiar un valor durante el layout en marcha re-calibra el sistema al instante.
|
||||
|
||||
### Stats line (sin vibracion)
|
||||
|
||||
Una sola linea fija — sin secciones condicionales que cambien la altura del panel:
|
||||
|
||||
```
|
||||
nodes=N edges=E energy=X fps=F | hover=#id cN sel=#id
|
||||
```
|
||||
|
||||
`hover` y `sel` muestran `-` cuando no hay nada seleccionado para mantener el ancho/alto estable; antes una fila condicional desplazaba el viewport en cada hover.
|
||||
|
||||
### Interaccion con el viewport
|
||||
|
||||
| Gesto | Accion |
|
||||
|---|---|
|
||||
| Drag con boton izquierdo en zona vacia | Pan de camara |
|
||||
| Wheel | Zoom (limites 0.01x – 50x) |
|
||||
| Drag sobre nodo | Mueve el nodo (lo `pin`ea durante el drag) |
|
||||
| Click sobre nodo | Selecciona (`s_state.selected_node`) |
|
||||
| Hover sobre nodo | Resaltado + `s_state.hovered_node` poblado |
|
||||
|
||||
### Datos sinteticos
|
||||
|
||||
`generate_synthetic_graph(N, K)` reparte N nodos en K clusters dispuestos en circulo, con ~3 aristas intra-cluster por nodo y un 5% adicional de aristas inter-cluster. Paleta de 8 colores ABGR. Posiciones iniciales con dispersion gaussiana de 80 px alrededor del centroide del cluster — el force layout las reordena en pocos frames.
|
||||
|
||||
### Performance esperada
|
||||
|
||||
| Nodes | FPS objetivo (RTX 30xx, viewport 800x460) | Notas |
|
||||
|---|---|---|
|
||||
| 1 000 | 60 (vsync) | Caso comun; layout converge < 1 s |
|
||||
| 5 000 | 60 | Pipeline al limite del CPU para Barnes-Hut |
|
||||
| 20 000 | 30 – 50 | El cuello pasa a ser el layout (CPU); GPU render sigue holgado |
|
||||
|
||||
Si necesitas mas, fija los nodos (`pinned = true` o `Pause layout`) y veras 60 fps estables — el bottleneck es la simulacion, no el render.
|
||||
|
||||
## Anadir un demo nuevo
|
||||
|
||||
1. Anadir el prototipo en `demos.h` dentro de `namespace gallery`:
|
||||
```cpp
|
||||
void demo_my_thing();
|
||||
```
|
||||
2. Implementar el cuerpo en `demos_core.cpp` o `demos_viz.cpp` (o un fichero nuevo si la demo es grande, p.ej. `demos_graph.cpp`).
|
||||
3. Registrar la entrada en el array `k_demos[]` de `main.cpp`:
|
||||
```cpp
|
||||
{"my_thing", "my_thing", "Core" /* o "Viz" */, &gallery::demo_my_thing},
|
||||
```
|
||||
4. Si la demo necesita `.cpp` adicionales del registry, anadirlos a `CMakeLists.txt` de la gallery.
|
||||
5. Recompilar.
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
cpp/apps/primitives_gallery/
|
||||
CMakeLists.txt # target primitives_gallery
|
||||
README.md # este fichero
|
||||
main.cpp # sidebar + router
|
||||
demo.{h,cpp} # helpers (demo_header, section, code_block, ...)
|
||||
demos.h # prototipos void demo_xxx()
|
||||
demos_core.cpp # demos del dominio core
|
||||
demos_viz.cpp # demos del dominio viz (charts simples)
|
||||
demos_graph.cpp # demo de graph_viewport (mas pesada, fichero aparte)
|
||||
```
|
||||
|
||||
## Convenciones para los demos
|
||||
|
||||
- **Sin estado real**: usar arrays sinteticos (`float fake[] = {...}`) o generadores deterministas con seed fijo. Datos reproducibles.
|
||||
- **Sin red**: nunca llamar a `sqlite_api`, HTTP, filesystem. La gallery debe arrancar offline en cualquier maquina.
|
||||
- **Snippets honestos**: el `code_block(...)` debe mostrar el codigo que produce esa demo, no pseudocodigo.
|
||||
- **Variantes en grids**: si un primitivo tiene N variantes x M tamanos, mostrarlos todos en un `BeginTable` para comparacion lado-a-lado.
|
||||
- **Estado static**: si la demo es interactiva (sliders, modal, etc.), guardar el estado en `static` locales — la gallery no destruye demos al cambiar de seccion, asi que el estado persiste hasta cerrar la app.
|
||||
|
||||
## Iconos en los demos
|
||||
|
||||
A partir de la sesion 2026-04-25 los demos usan los macros `TI_*` de `cpp/functions/core/icons_tabler.h` (Tabler v3.41.1, 5093 glyphs). La fuente la carga automaticamente `fn::run_app` via `icon_font_cpp_core`, y `add_imgui_app` copia `tabler-icons.ttf` junto al ejecutable post-build (no hay paso manual).
|
||||
|
||||
`demo_icon_button` y `demo_toolbar` (en `demos_core.cpp`) son la referencia visual: muestran el patron `button(TI_PLUS " New", V::Primary)` y la fila de iconos sueltos. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla.
|
||||
|
||||
Si añades un demo nuevo y necesitas glyphs, **no metas `\x..` UTF-8 inline** — busca el icono en `icons_tabler.h` (o en https://tabler.io/icons) y usa el `TI_*` correspondiente.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: primitives_gallery
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
description: "Visual catalog de primitivas C++ UI del fn_registry. Demos por categoria (charts, controls, layout, gl_info). Soporta modo --capture para regresion visual."
|
||||
tags: [imgui, gallery, gfx, demo, capture]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
dir_path: "cpp/apps/primitives_gallery"
|
||||
repo_url: ""
|
||||
---
|
||||
|
||||
# primitives_gallery
|
||||
|
||||
Catalogo visual de las primitivas y componentes ImGui del registry. Cada demo se carga al hacer click en su entrada del sidebar.
|
||||
|
||||
## Build & run
|
||||
|
||||
```bash
|
||||
cd cpp && cmake --build build --target primitives_gallery -j
|
||||
./build/primitives_gallery
|
||||
```
|
||||
|
||||
## Modo capture (regresion visual)
|
||||
|
||||
```bash
|
||||
./build/primitives_gallery --capture <out_dir>
|
||||
```
|
||||
|
||||
Renderiza cada demo offscreen y guarda PNGs en `<out_dir>/`. Permite gate visual via golden images.
|
||||
|
||||
## Notas
|
||||
|
||||
- `auto_dockspace = false` — usa `fullscreen_window` que ocupa todo el viewport.
|
||||
- `init_gl_loader = true` — necesario para demos de OpenGL 4.3 core (compute, SSBOs).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 966 B |
@@ -1,173 +0,0 @@
|
||||
// Implementacion de gallery::run_capture — render offscreen + glReadPixels +
|
||||
// PNG via stb_image_write. Ver capture.h.
|
||||
|
||||
#include "capture.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_glfw.h"
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "implot.h"
|
||||
#include "implot3d.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/icon_font.h"
|
||||
#include "core/app_settings.h"
|
||||
#include "gfx/gl_loader.h"
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image_write.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
static void glfw_capture_error(int error, const char* description) {
|
||||
std::fprintf(stderr, "GLFW Error %d: %s\n", error, description);
|
||||
}
|
||||
|
||||
// Flip vertical in-place: OpenGL origin = bottom-left, PNG = top-left.
|
||||
static void flip_vertical_rgba(unsigned char* px, int w, int h) {
|
||||
const int stride = w * 4;
|
||||
std::vector<unsigned char> row(stride);
|
||||
for (int y = 0; y < h / 2; ++y) {
|
||||
unsigned char* a = px + y * stride;
|
||||
unsigned char* b = px + (h - 1 - y) * stride;
|
||||
std::copy(a, a + stride, row.begin());
|
||||
std::copy(b, b + stride, a);
|
||||
std::copy(row.begin(), row.end(), b);
|
||||
}
|
||||
}
|
||||
|
||||
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items) {
|
||||
glfwSetErrorCallback(&glfw_capture_error);
|
||||
if (!glfwInit()) {
|
||||
std::fprintf(stderr, "capture: glfwInit failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capture mode usa GL 3.3 deliberadamente: WSL Mesa no entrega contexto
|
||||
// 4.3 offscreen (GLXBadFBConfig). Las pruebas visuales no necesitan
|
||||
// compute/SSBO — ImGui+ImPlot funciona en 3.3 core. La build interactiva
|
||||
// (app_base.cpp) si pide 4.3.
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
#ifdef __APPLE__
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
||||
#endif
|
||||
|
||||
GLFWwindow* window = glfwCreateWindow(
|
||||
cfg.capture_w, cfg.capture_h, "capture", nullptr, nullptr);
|
||||
if (!window) {
|
||||
std::fprintf(stderr, "capture: glfwCreateWindow failed (no GL?)\n");
|
||||
glfwTerminate();
|
||||
return false;
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent(window);
|
||||
glfwSwapInterval(0);
|
||||
|
||||
if (!fn::gfx::gl_loader_init()) {
|
||||
std::fprintf(stderr, "capture: gl_loader_init failed\n");
|
||||
glfwDestroyWindow(window);
|
||||
glfwTerminate();
|
||||
return false;
|
||||
}
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImPlot::CreateContext();
|
||||
ImPlot3D::CreateContext();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.IniFilename = nullptr; // no .ini side effects in capture mode.
|
||||
io.DisplaySize = ImVec2((float)cfg.capture_w, (float)cfg.capture_h);
|
||||
|
||||
fn_ui::settings_load();
|
||||
fn_ui::load_fonts_from_settings();
|
||||
{
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style.FontSizeBase = fn_ui::settings().font_size_px;
|
||||
style._NextFrameFontSizeBase = style.FontSizeBase;
|
||||
}
|
||||
|
||||
fn_tokens::apply_dark_theme();
|
||||
|
||||
ImGui_ImplGlfw_InitForOpenGL(window, false);
|
||||
ImGui_ImplOpenGL3_Init("#version 330");
|
||||
|
||||
bool ok_all = true;
|
||||
std::vector<unsigned char> pixels((size_t)cfg.capture_w * cfg.capture_h * 4u);
|
||||
|
||||
for (const auto& item : items) {
|
||||
// Warmup: rinde varios frames para que ImGui/ImPlot estabilicen layout
|
||||
// (el primer frame frecuentemente carece de mediciones de tamaño).
|
||||
for (int frame = 0; frame < cfg.warmup_frames + 1; ++frame) {
|
||||
glfwPollEvents();
|
||||
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplGlfw_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
// Ventana fullscreen sobre el viewport con la demo activa,
|
||||
// sin sidebar (queremos el render del primitivo lo mas limpio
|
||||
// posible para el diff visual).
|
||||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(vp->WorkPos);
|
||||
ImGui::SetNextWindowSize(vp->WorkSize);
|
||||
ImGui::Begin("##capture_root",
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
if (item.fn) item.fn();
|
||||
ImGui::End();
|
||||
|
||||
ImGui::Render();
|
||||
int dw, dh;
|
||||
glfwGetFramebufferSize(window, &dw, &dh);
|
||||
glViewport(0, 0, dw, dh);
|
||||
glClearColor(fn_tokens::colors::bg.x,
|
||||
fn_tokens::colors::bg.y,
|
||||
fn_tokens::colors::bg.z, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
glfwSwapBuffers(window);
|
||||
}
|
||||
|
||||
// Read framebuffer (GL_RGBA / GL_UNSIGNED_BYTE).
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, cfg.capture_w, cfg.capture_h,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
|
||||
flip_vertical_rgba(pixels.data(), cfg.capture_w, cfg.capture_h);
|
||||
|
||||
char path[1024];
|
||||
std::snprintf(path, sizeof(path), "%s/%s.png",
|
||||
cfg.output_dir.c_str(), item.id.c_str());
|
||||
const int rc = stbi_write_png(
|
||||
path, cfg.capture_w, cfg.capture_h, 4,
|
||||
pixels.data(), cfg.capture_w * 4);
|
||||
if (rc == 0) {
|
||||
std::fprintf(stderr, "capture: stbi_write_png failed for %s\n", path);
|
||||
ok_all = false;
|
||||
} else {
|
||||
std::fprintf(stdout, "captured: %s\n", path);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
ImPlot3D::DestroyContext();
|
||||
ImPlot::DestroyContext();
|
||||
ImGui::DestroyContext();
|
||||
glfwDestroyWindow(window);
|
||||
glfwTerminate();
|
||||
return ok_all;
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
// Capture mode: renderiza cada demo de la gallery en una ventana GLFW
|
||||
// invisible y guarda un PNG en `output_dir/<demo_id>.png` via stb_image_write.
|
||||
//
|
||||
// Diseñado para CI / golden-image diffing: ver `cpp/scripts/update_goldens.sh`
|
||||
// y `cpp/tests/test_visual.cpp`.
|
||||
//
|
||||
// Importante:
|
||||
// - Requiere un contexto OpenGL real. En entornos sin GPU (containers minimos)
|
||||
// funciona con `LIBGL_ALWAYS_SOFTWARE=1` (Mesa/llvmpipe) o swiftshader.
|
||||
// - Si el entorno (WSL sin GL) no puede crear un contexto GL valido, el
|
||||
// binario sale con codigo != 0 sin generar PNGs.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
struct CaptureItem {
|
||||
std::string id;
|
||||
void (*fn)();
|
||||
};
|
||||
|
||||
struct CaptureConfig {
|
||||
std::string output_dir;
|
||||
int warmup_frames = 3;
|
||||
int capture_w = 800;
|
||||
int capture_h = 600;
|
||||
};
|
||||
|
||||
// Devuelve true si todo el set se capturo OK.
|
||||
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items);
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,76 +0,0 @@
|
||||
#include "demo.h"
|
||||
#include "core/tokens.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
void demo_header(const char* name, const char* version, const char* description) {
|
||||
using namespace fn_tokens;
|
||||
|
||||
ImGui::SetWindowFontScale(1.4f);
|
||||
ImGui::TextUnformatted(name);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::Text(" %s", version);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (description && *description) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::TextWrapped("%s", description);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, spacing::sm));
|
||||
}
|
||||
|
||||
void section(const char* title) {
|
||||
using namespace fn_tokens;
|
||||
ImGui::Dummy(ImVec2(0, spacing::sm));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::TextUnformatted(title);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, spacing::xs));
|
||||
}
|
||||
|
||||
void variant_label(const char* text) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void code_block(const char* code) {
|
||||
using namespace fn_tokens;
|
||||
ImGui::Dummy(ImVec2(0, spacing::sm));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextUnformatted("// example");
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
|
||||
|
||||
// Altura: aprox lineas * line-height
|
||||
int lines = 1;
|
||||
for (const char* p = code; *p; ++p) if (*p == '\n') ++lines;
|
||||
float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md;
|
||||
|
||||
char id[32];
|
||||
std::snprintf(id, sizeof(id), "##code_%p", (const void*)code);
|
||||
ImGui::BeginChild(id, ImVec2(0, h),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
|
||||
ImGui::TextUnformatted(code);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
// Helpers compartidos por todas las demos de la gallery.
|
||||
// No son primitivos del registry — son utilidades locales de este app.
|
||||
|
||||
#include "imgui.h"
|
||||
#include <string>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// Titulo + version + descripcion en la parte superior del panel derecho.
|
||||
void demo_header(const char* name, const char* version, const char* description);
|
||||
|
||||
// Seccion secundaria dentro de una demo (agrupar variantes).
|
||||
void section(const char* title);
|
||||
|
||||
// Bloque de codigo monoespaciado con bg surface y label "// example".
|
||||
void code_block(const char* code);
|
||||
|
||||
// Etiqueta sutil encima de un grupo de widgets.
|
||||
void variant_label(const char* text);
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,56 +0,0 @@
|
||||
#pragma once
|
||||
// Cada demo_xxx() renderiza una seccion completa para un primitivo.
|
||||
// Se llaman desde main.cpp en funcion del item seleccionado en el sidebar.
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// --- Core ---
|
||||
void demo_button();
|
||||
void demo_icon_button();
|
||||
void demo_toolbar();
|
||||
void demo_modal();
|
||||
void demo_text_input();
|
||||
void demo_select();
|
||||
void demo_toast();
|
||||
void demo_tree_view();
|
||||
void demo_kpi_card();
|
||||
void demo_badge();
|
||||
void demo_empty_state();
|
||||
void demo_page_header();
|
||||
void demo_dashboard_panel();
|
||||
void demo_text_editor(); // wave 1, issue 0025
|
||||
void demo_file_watcher(); // wave 1, issue 0025
|
||||
void demo_process_runner();
|
||||
void demo_tween(); // issue 0031
|
||||
void demo_bezier_editor(); // issue 0031
|
||||
void demo_timeline(); // issue 0031
|
||||
void demo_sql_workbench(); // issue 0032
|
||||
|
||||
// --- Viz ---
|
||||
void demo_bar_chart();
|
||||
void demo_pie_chart();
|
||||
void demo_line_plot();
|
||||
void demo_scatter_plot();
|
||||
void demo_histogram();
|
||||
void demo_sparkline();
|
||||
void demo_graph();
|
||||
void demo_graph_styles(); // issue 0049f
|
||||
void demo_candlestick();
|
||||
void demo_gauge();
|
||||
void demo_heatmap();
|
||||
void demo_table_view();
|
||||
void demo_surface_plot_3d(); // issue 0028, ImPlot3D
|
||||
void demo_scatter_3d(); // issue 0028, ImPlot3D
|
||||
void demo_mesh_viewer(); // issue 0029
|
||||
void demo_treemap(); // issue 0034
|
||||
void demo_sankey(); // issue 0034
|
||||
void demo_chord(); // issue 0034
|
||||
void demo_contour(); // issue 0034
|
||||
void demo_voronoi(); // issue 0034
|
||||
|
||||
// --- Gfx ---
|
||||
void demo_shader_canvas();
|
||||
void demo_gl_texture(); // wave 1, issue 0026
|
||||
void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,100 +0,0 @@
|
||||
// demos_3d — demos para los primitivos viz/* basados en ImPlot3D.
|
||||
// Issue 0028: surface_plot_3d real + scatter_3d.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/surface_plot_3d.h"
|
||||
#include "viz/scatter_3d.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// surface_plot_3d
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_surface_plot_3d() {
|
||||
demo_header("surface_plot_3d", "v2.0.0",
|
||||
"Superficie 3D ImPlot3D (z = A * sin(fx*x) * cos(fy*y)) con sliders para "
|
||||
"ajustar las frecuencias en tiempo real. Drag para orbitar, wheel para zoom.");
|
||||
|
||||
section("Malla 64x64 — sin(fx*x) * cos(fy*y)");
|
||||
|
||||
static float fx = 0.20f;
|
||||
static float fy = 0.20f;
|
||||
static float amp = 1.0f;
|
||||
|
||||
ImGui::SliderFloat("fx", &fx, 0.05f, 1.0f, "%.2f");
|
||||
ImGui::SliderFloat("fy", &fy, 0.05f, 1.0f, "%.2f");
|
||||
ImGui::SliderFloat("amplitud", &, 0.1f, 3.0f, "%.2f");
|
||||
|
||||
constexpr int N = 64;
|
||||
static std::vector<float> z(N * N);
|
||||
for (int j = 0; j < N; ++j) {
|
||||
for (int i = 0; i < N; ++i) {
|
||||
z[j * N + i] = amp * std::sin(fx * float(i)) * std::cos(fy * float(j));
|
||||
}
|
||||
}
|
||||
|
||||
fn::SurfacePlot3DConfig cfg{};
|
||||
cfg.z = z.data();
|
||||
cfg.nx = N; cfg.ny = N;
|
||||
cfg.x_min = 0.f; cfg.x_max = float(N);
|
||||
cfg.y_min = 0.f; cfg.y_max = float(N);
|
||||
cfg.size = ImVec2(-1.f, 420.f);
|
||||
fn::surface_plot_3d("##gallery_surface", cfg);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// scatter_3d
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_scatter_3d() {
|
||||
demo_header("scatter_3d", "v1.0.0",
|
||||
"Scatter 3D ImPlot3D con color por punto. 3 clusters gaussianos sinteticos "
|
||||
"(N=500) para simular una visualizacion tipica de PCA / clustering.");
|
||||
|
||||
section("3 clusters gaussianos (500 puntos)");
|
||||
|
||||
constexpr int N = 500;
|
||||
static std::vector<float> xs(N), ys(N), zs(N);
|
||||
static std::vector<ImU32> colors(N);
|
||||
static bool initialized = false;
|
||||
|
||||
if (!initialized) {
|
||||
std::mt19937 rng(42);
|
||||
std::normal_distribution<float> g(0.f, 0.4f);
|
||||
const ImU32 palette[3] = {
|
||||
IM_COL32(255, 99, 71, 255), // tomate
|
||||
IM_COL32( 65, 170, 255, 255), // azul
|
||||
IM_COL32(120, 220, 120, 255), // verde
|
||||
};
|
||||
const float cx[3] = {-1.5f, 1.5f, 0.f};
|
||||
const float cy[3] = { 0.f, 0.f, 2.0f};
|
||||
const float cz[3] = { 0.f, 1.0f,-1.0f};
|
||||
for (int i = 0; i < N; ++i) {
|
||||
int c = i % 3;
|
||||
xs[i] = cx[c] + g(rng);
|
||||
ys[i] = cy[c] + g(rng);
|
||||
zs[i] = cz[c] + g(rng);
|
||||
colors[i] = palette[c];
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
fn::Scatter3DConfig cfg{};
|
||||
cfg.xs = xs.data();
|
||||
cfg.ys = ys.data();
|
||||
cfg.zs = zs.data();
|
||||
cfg.colors = colors.data();
|
||||
cfg.n = N;
|
||||
cfg.size = ImVec2(-1.f, 420.f);
|
||||
fn::scatter_3d("##gallery_clusters", cfg);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,249 +0,0 @@
|
||||
// Demos para los primitivos de animacion (issue 0031):
|
||||
// - tween_curves
|
||||
// - bezier_editor
|
||||
// - timeline
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "core/tween_curves.h"
|
||||
#include "core/bezier_editor.h"
|
||||
#include "core/timeline.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// demo_tween — dropdown + plot animado
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_tween() {
|
||||
using namespace fn_tokens;
|
||||
using fn::tween::Ease;
|
||||
|
||||
demo_header("tween_curves", "v1.0.0",
|
||||
"Funciones de easing (Penner): linear, quad, cubic, expo, elastic, "
|
||||
"bounce con variantes in/out/inOut. Header-mostly: el compilador "
|
||||
"inlinea cada curva en el sitio de llamada.");
|
||||
|
||||
section("Selector + plot");
|
||||
|
||||
static int ease_idx = (int)Ease::OutCubic;
|
||||
static float anim_t = 0.0f;
|
||||
anim_t += ImGui::GetIO().DeltaTime * 0.5f;
|
||||
if (anim_t > 1.5f) anim_t = -0.25f; // hold un poco antes de reiniciar
|
||||
|
||||
// Build labels
|
||||
const char* labels[fn::tween::ease_count];
|
||||
for (int i = 0; i < fn::tween::ease_count; i++) {
|
||||
labels[i] = fn::tween::name((Ease)i);
|
||||
}
|
||||
ImGui::SetNextItemWidth(220.0f);
|
||||
ImGui::Combo("##tween_ease", &ease_idx, labels, fn::tween::ease_count);
|
||||
|
||||
Ease ease = (Ease)ease_idx;
|
||||
float t_clamped = anim_t;
|
||||
if (t_clamped < 0.0f) t_clamped = 0.0f;
|
||||
if (t_clamped > 1.0f) t_clamped = 1.0f;
|
||||
float v = fn::tween::apply(ease, t_clamped);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::Text(" t=%.2f f(t)=%.3f", t_clamped, v);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Canvas plot
|
||||
ImVec2 canvas_min = ImGui::GetCursorScreenPos();
|
||||
ImVec2 canvas_size(360.0f, 220.0f);
|
||||
ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x, canvas_min.y + canvas_size.y);
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm);
|
||||
dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm);
|
||||
|
||||
auto to_px = [&](float tx, float ty) {
|
||||
// ty puede salir de [0,1] (elastic/bounce); damos algo de margen vertical.
|
||||
return ImVec2(canvas_min.x + tx * canvas_size.x,
|
||||
canvas_min.y + (1.0f - ty) * canvas_size.y);
|
||||
};
|
||||
|
||||
// Grid 4x4
|
||||
ImU32 grid = ImGui::GetColorU32(colors::border);
|
||||
for (int i = 1; i < 4; i++) {
|
||||
float fx = canvas_min.x + canvas_size.x * (float)i / 4.0f;
|
||||
float fy = canvas_min.y + canvas_size.y * (float)i / 4.0f;
|
||||
dl->AddLine(ImVec2(fx, canvas_min.y), ImVec2(fx, canvas_max.y), grid);
|
||||
dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid);
|
||||
}
|
||||
|
||||
// Diagonal linear
|
||||
dl->AddLine(to_px(0.0f, 0.0f), to_px(1.0f, 1.0f),
|
||||
ImGui::GetColorU32(colors::text_dim), 1.0f);
|
||||
|
||||
// Curva
|
||||
constexpr int N = 96;
|
||||
ImVec2 prev = to_px(0.0f, fn::tween::apply(ease, 0.0f));
|
||||
ImU32 col = ImGui::GetColorU32(colors::primary);
|
||||
for (int i = 1; i <= N; i++) {
|
||||
float x = (float)i / (float)N;
|
||||
float y = fn::tween::apply(ease, x);
|
||||
ImVec2 cur = to_px(x, y);
|
||||
dl->AddLine(prev, cur, col, 2.0f);
|
||||
prev = cur;
|
||||
}
|
||||
|
||||
// Marker animado
|
||||
ImVec2 m = to_px(t_clamped, v);
|
||||
dl->AddCircleFilled(m, 5.0f, ImGui::GetColorU32(colors::primary_light));
|
||||
dl->AddCircle(m, 6.0f, ImGui::GetColorU32(colors::text), 0, 1.5f);
|
||||
|
||||
// Avanzar cursor
|
||||
ImGui::Dummy(canvas_size);
|
||||
|
||||
code_block(
|
||||
"#include \"core/tween_curves.h\"\n\n"
|
||||
"float k = fn::tween::apply(fn::tween::Ease::OutCubic, t);\n"
|
||||
"// o named:\n"
|
||||
"float k2 = fn::tween::out_cubic(t);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// demo_bezier_editor — editor + plot evaluado
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_bezier_editor() {
|
||||
using namespace fn_tokens;
|
||||
|
||||
demo_header("bezier_editor", "v1.0.0",
|
||||
"Editor visual de curva Bezier cubica (4 puntos). Para diseñar "
|
||||
"easing curves custom. p1/p2 son draggable; p0/p3 fijos en (0,0)/(1,1).");
|
||||
|
||||
section("Editor");
|
||||
|
||||
static fn::BezierCurve curve; // identidad por defecto: ease lineal con handles desplazados
|
||||
|
||||
if (ImGui::Button("Reset##bz_reset")) {
|
||||
curve = fn::BezierCurve{};
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Ease-out preset##bz_eo")) {
|
||||
curve = {{0,0}, {0.0f, 0.0f}, {0.58f, 1.0f}, {1,1}};
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Ease-in-out preset##bz_eio")) {
|
||||
curve = {{0,0}, {0.42f, 0.0f}, {0.58f, 1.0f}, {1,1}};
|
||||
}
|
||||
|
||||
fn::bezier_editor("##bz_editor", curve, ImVec2(220, 220));
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::Text("p0=(%.2f,%.2f) p1=(%.2f,%.2f) p2=(%.2f,%.2f) p3=(%.2f,%.2f)",
|
||||
curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y,
|
||||
curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Plot evaluation
|
||||
section("bezier_eval(curve, t)");
|
||||
static float t = 0.0f;
|
||||
ImGui::SetNextItemWidth(360.0f);
|
||||
ImGui::SliderFloat("t##bz_t", &t, 0.0f, 1.0f, "%.3f");
|
||||
float y = fn::bezier_eval(curve, t);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::Text("y(t=%.3f) = %.3f", t, y);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
code_block(
|
||||
"#include \"core/bezier_editor.h\"\n\n"
|
||||
"static fn::BezierCurve curve;\n"
|
||||
"if (fn::bezier_editor(\"##my\", curve, ImVec2(220, 220))) {\n"
|
||||
" // user dragged a control point\n"
|
||||
"}\n"
|
||||
"float k = fn::bezier_eval(curve, t);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// demo_timeline — 2 tracks + display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_timeline() {
|
||||
using namespace fn_tokens;
|
||||
using fn::tween::Ease;
|
||||
|
||||
demo_header("timeline", "v1.0.0",
|
||||
"Timeline tipo DAW: tracks horizontales con keyframes draggable, "
|
||||
"scrub con el ruler, play/pause/loop. track_value_at(t) interpola "
|
||||
"aplicando la Ease de cada keyframe destino.");
|
||||
|
||||
static fn::TimelineState tl;
|
||||
static bool inited = false;
|
||||
if (!inited) {
|
||||
tl.duration = 4.0f;
|
||||
tl.playing = true;
|
||||
tl.tracks.push_back({"hue", {
|
||||
{0.0f, 0.0f, Ease::Linear},
|
||||
{2.0f, 1.0f, Ease::OutCubic},
|
||||
{4.0f, 0.0f, Ease::InOutCubic},
|
||||
}});
|
||||
tl.tracks.push_back({"amp", {
|
||||
{0.0f, 0.2f, Ease::Linear},
|
||||
{3.0f, 1.0f, Ease::OutElastic},
|
||||
}});
|
||||
inited = true;
|
||||
}
|
||||
|
||||
// Update
|
||||
fn::timeline_update(tl, ImGui::GetIO().DeltaTime);
|
||||
|
||||
// Display values
|
||||
section("Live values");
|
||||
float hue = fn::track_value_at(tl.tracks[0], tl.current_time);
|
||||
float amp = fn::track_value_at(tl.tracks[1], tl.current_time);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
|
||||
ImGui::Text("t = %.3fs", tl.current_time);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto draw_bar = [&](const char* name, float value, float vmin, float vmax) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::Text("%-4s", name);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImVec2 cmin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 csize = ImVec2(280.0f, 14.0f);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x, cmin.y + csize.y),
|
||||
ImGui::GetColorU32(colors::surface_active), radius::sm);
|
||||
float k = (value - vmin) / (vmax - vmin);
|
||||
if (k < 0.0f) k = 0.0f;
|
||||
if (k > 1.0f) k = 1.0f;
|
||||
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x * k, cmin.y + csize.y),
|
||||
ImGui::GetColorU32(colors::primary), radius::sm);
|
||||
ImGui::Dummy(csize);
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%.3f", value);
|
||||
};
|
||||
draw_bar("hue", hue, 0.0f, 1.0f);
|
||||
draw_bar("amp", amp, 0.0f, 1.0f);
|
||||
|
||||
section("Widget");
|
||||
fn::timeline_widget("##gallery_tl", tl, ImVec2(-1, 220));
|
||||
|
||||
code_block(
|
||||
"#include \"core/timeline.h\"\n\n"
|
||||
"static fn::TimelineState tl;\n"
|
||||
"tl.tracks.push_back({\"hue\", {{0,0}, {2,1, fn::tween::Ease::OutCubic}, {4,0}}});\n"
|
||||
"tl.duration = 4.0f; tl.playing = true;\n\n"
|
||||
"fn::timeline_update(tl, ImGui::GetIO().DeltaTime);\n"
|
||||
"float h = fn::track_value_at(tl.tracks[0], tl.current_time);\n"
|
||||
"fn::timeline_widget(\"##tl\", tl);"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,447 +0,0 @@
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "core/button.h"
|
||||
#include "core/icon_button.h"
|
||||
#include "core/toolbar.h"
|
||||
#include "core/modal_dialog.h"
|
||||
#include "core/text_input.h"
|
||||
#include "core/select.h"
|
||||
#include "core/toast.h"
|
||||
#include "core/tree_view.h"
|
||||
#include "core/badge.h"
|
||||
#include "core/empty_state.h"
|
||||
#include "core/page_header.h"
|
||||
#include "core/dashboard_panel.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/icons_tabler.h"
|
||||
#include "viz/kpi_card.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace fn_ui;
|
||||
using V = ButtonVariant;
|
||||
using S = ButtonSize;
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// button
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_button() {
|
||||
demo_header("button", "v1.0.0",
|
||||
"Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, "
|
||||
"radius y padding — estilo consistente en toda la app.");
|
||||
|
||||
section("Variants x Sizes");
|
||||
const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger};
|
||||
const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"};
|
||||
const S sizes[] = {S::Sm, S::Md, S::Lg};
|
||||
const char* size_names[] = {"sm", "md", "lg"};
|
||||
|
||||
if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) {
|
||||
ImGui::TableSetupColumn("size");
|
||||
for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (int s = 0; s < 3; s++) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
variant_label(size_names[s]);
|
||||
for (int v = 0; v < 4; v++) {
|
||||
ImGui::TableSetColumnIndex(v + 1);
|
||||
char id[32];
|
||||
std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v);
|
||||
button(id, variants[v], sizes[s]);
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
code_block(
|
||||
"#include \"core/button.h\"\n"
|
||||
"using fn_ui::button;\n"
|
||||
"using V = fn_ui::ButtonVariant;\n\n"
|
||||
"if (button(\"Save\", V::Primary)) save();\n"
|
||||
"if (button(\"Cancel\", V::Subtle)) close();\n"
|
||||
"if (button(\"Delete\", V::Danger)) confirm();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// icon_button
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_icon_button() {
|
||||
demo_header("icon_button", "v1.0.0",
|
||||
"Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. "
|
||||
"Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente "
|
||||
"por fn::run_app via icon_font.cpp).");
|
||||
|
||||
section("Tabler icon set");
|
||||
struct { const char* id; const char* glyph; const char* tip; } ic[] = {
|
||||
{"##rl", TI_REFRESH, "Reload"},
|
||||
{"##ad", TI_PLUS, "Add"},
|
||||
{"##dl", TI_TRASH, "Delete"},
|
||||
{"##dn", TI_CHEVRON_DOWN, "Dropdown"},
|
||||
{"##cf", TI_SETTINGS, "Settings"},
|
||||
{"##ok", TI_CHECK, "Check"},
|
||||
{"##cl", TI_X, "Close"},
|
||||
{"##ed", TI_PENCIL, "Edit"},
|
||||
{"##sv", TI_DEVICE_FLOPPY, "Save"},
|
||||
{"##sr", TI_SEARCH, "Search"},
|
||||
{"##hp", TI_HELP, "Help"},
|
||||
{"##hm", TI_HOME, "Home"},
|
||||
};
|
||||
for (auto& b : ic) {
|
||||
icon_button(b.id, b.glyph, b.tip);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::NewLine();
|
||||
|
||||
code_block(
|
||||
"#include \"core/icons_tabler.h\"\n\n"
|
||||
"if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n"
|
||||
" reload_data();\n\n"
|
||||
"// Mas de 5000 iconos disponibles — ver core/icons_tabler.h"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toolbar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_toolbar() {
|
||||
demo_header("toolbar", "v1.0.0",
|
||||
"Grupo horizontal con spacing consistente y separadores verticales sutiles. "
|
||||
"El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos.");
|
||||
|
||||
section("Example with two groups");
|
||||
toolbar_begin();
|
||||
button(TI_PLUS " New", V::Primary); ImGui::SameLine();
|
||||
button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine();
|
||||
button(TI_DEVICE_FLOPPY " Save",V::Secondary);
|
||||
toolbar_separator();
|
||||
icon_button("##set", TI_SETTINGS, "Settings");
|
||||
ImGui::SameLine();
|
||||
icon_button("##help", TI_HELP, "Help");
|
||||
toolbar_end();
|
||||
|
||||
code_block(
|
||||
"#include \"core/icons_tabler.h\"\n\n"
|
||||
"toolbar_begin();\n"
|
||||
" button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n"
|
||||
" button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n"
|
||||
" toolbar_separator();\n"
|
||||
" icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n"
|
||||
"toolbar_end();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// modal_dialog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_modal() {
|
||||
demo_header("modal_dialog", "v1.0.0",
|
||||
"Popup modal centrada con estilo surface+border. Close con Escape o click en X. "
|
||||
"Patron begin/end — modal_dialog_end debe llamarse siempre.");
|
||||
|
||||
static bool show = false;
|
||||
if (button("Open modal", V::Primary)) show = true;
|
||||
|
||||
if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) {
|
||||
ImGui::TextWrapped(
|
||||
"Modal centrada en el viewport principal, con estilo tokens.");
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
||||
static char buf[64] = {};
|
||||
text_input("Name", buf, sizeof(buf), "escribe algo");
|
||||
ImGui::Separator();
|
||||
if (button("Cancel", V::Subtle)) show = false;
|
||||
ImGui::SameLine();
|
||||
if (button("Done", V::Primary)) show = false;
|
||||
}
|
||||
modal_dialog_end();
|
||||
|
||||
code_block(
|
||||
"static bool show = false;\n"
|
||||
"if (button(\"Open\", Primary)) show = true;\n"
|
||||
"if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n"
|
||||
" // ... campos del form ...\n"
|
||||
" if (button(\"Done\", Primary)) show = false;\n"
|
||||
"}\n"
|
||||
"modal_dialog_end();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// text_input
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_text_input() {
|
||||
demo_header("text_input", "v1.0.0",
|
||||
"Label muted + input estilizado con tokens. Full-width dentro del contenedor. "
|
||||
"Placeholder opcional mostrado en text_dim cuando el buffer esta vacio.");
|
||||
|
||||
static char name[128] = {};
|
||||
static char desc[256] = {};
|
||||
static char tags[128] = {};
|
||||
|
||||
ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
|
||||
text_input("Name", name, sizeof(name), "my-new-thing");
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
|
||||
text_input("Description", desc, sizeof(desc));
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
|
||||
text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form");
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"static char name[128] = {};\n"
|
||||
"text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n"
|
||||
"// true on change — se usa mas para validar en vivo\n"
|
||||
"// que para leer el valor (que vive en el buffer)."
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// select
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_select() {
|
||||
demo_header("select", "v1.0.0",
|
||||
"Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input.");
|
||||
|
||||
static int lang_idx = 0;
|
||||
static int domain_idx = -1;
|
||||
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
|
||||
const char* domains[] = {"core", "infra", "finance", "datascience", "viz"};
|
||||
|
||||
ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
|
||||
select("Language", &lang_idx, langs, 5);
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
|
||||
select("Domain (optional)", &domain_idx, domains, 5, true);
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"static int lang = 0;\n"
|
||||
"const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
|
||||
"select(\"Language\", &lang, langs, 5);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toast + inbox
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_toast() {
|
||||
demo_header("toast", "v1.1.0",
|
||||
"Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. "
|
||||
"La campana muestra badge con no-leidos y popover con las ultimas 50.");
|
||||
|
||||
section("Trigger toasts");
|
||||
if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast");
|
||||
ImGui::SameLine();
|
||||
if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed");
|
||||
ImGui::SameLine();
|
||||
if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something");
|
||||
ImGui::SameLine();
|
||||
if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason");
|
||||
|
||||
section("Inbox (bell with unread badge)");
|
||||
toast_inbox_button("##inbox_demo");
|
||||
|
||||
code_block(
|
||||
"toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n"
|
||||
"toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n"
|
||||
"// En la toolbar:\n"
|
||||
"toast_inbox_button(\"##inbox\");\n\n"
|
||||
"// Una vez por frame al final del render:\n"
|
||||
"toast_render();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tree_view
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_tree_view() {
|
||||
demo_header("tree_view", "v1.0.0",
|
||||
"Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). "
|
||||
"Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro.");
|
||||
|
||||
static std::string selected;
|
||||
|
||||
section("Projects (fake)");
|
||||
ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders);
|
||||
|
||||
struct FakeProject { const char* id; const char* name; const char* apps[3]; };
|
||||
const FakeProject projs[] = {
|
||||
{"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}},
|
||||
{"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}},
|
||||
{"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}},
|
||||
};
|
||||
for (auto& p : projs) {
|
||||
bool sel = (selected == p.id);
|
||||
if (tree_branch_begin(p.id, p.name, sel)) {
|
||||
if (tree_node_clicked()) selected = p.id;
|
||||
for (int i = 0; i < 3 && p.apps[i]; i++) {
|
||||
bool asel = (selected == p.apps[i]);
|
||||
tree_leaf(p.apps[i], p.apps[i], asel);
|
||||
if (tree_node_clicked()) selected = p.apps[i];
|
||||
}
|
||||
tree_branch_end();
|
||||
} else if (tree_node_clicked()) {
|
||||
selected = p.id;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||
ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
code_block(
|
||||
"static std::string sel;\n"
|
||||
"if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n"
|
||||
" if (tree_node_clicked()) sel = p.id;\n"
|
||||
" for (auto& a : p.apps) {\n"
|
||||
" tree_leaf(a.id, a.name, sel == a.id);\n"
|
||||
" if (tree_node_clicked()) sel = a.id;\n"
|
||||
" }\n"
|
||||
" tree_branch_end();\n"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// kpi_card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_kpi_card() {
|
||||
demo_header("kpi_card", "v1.3.0",
|
||||
"Card compacta 86px con icono opcional + label muted, valor x1.4, trend con "
|
||||
"TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md.");
|
||||
|
||||
if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
|
||||
float history[] = {10, 12, 11, 15, 18, 17, 20};
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
|
||||
ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS);
|
||||
ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR);
|
||||
ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE);
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
code_block(
|
||||
"#include \"core/icons_tabler.h\"\n\n"
|
||||
"float history[] = {10,12,11,15,18,17,20};\n"
|
||||
"kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n"
|
||||
"kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n"
|
||||
"// Sin delta ni history: muestra TI_MINUS como placeholder\n"
|
||||
"kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// badge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_badge() {
|
||||
demo_header("badge", "v1.0.0",
|
||||
"Etiqueta inline con 6 variantes semanticas. Equivalente a <Badge> de fn_library.");
|
||||
|
||||
section("Variants");
|
||||
badge("Default", BadgeVariant::Default); ImGui::SameLine();
|
||||
badge("Success", BadgeVariant::Success); ImGui::SameLine();
|
||||
badge("Warning", BadgeVariant::Warning); ImGui::SameLine();
|
||||
badge("Error", BadgeVariant::Error); ImGui::SameLine();
|
||||
badge("Info", BadgeVariant::Info); ImGui::SameLine();
|
||||
badge("Outline", BadgeVariant::Outline);
|
||||
|
||||
section("In context (table row)");
|
||||
ImGui::Text("filter_slice_go_core"); ImGui::SameLine();
|
||||
badge("pure", BadgeVariant::Success); ImGui::SameLine();
|
||||
badge("tested", BadgeVariant::Info);
|
||||
|
||||
code_block(
|
||||
"badge(\"pure\", BadgeVariant::Success);\n"
|
||||
"badge(\"stale\", BadgeVariant::Warning);\n"
|
||||
"badge(\"broken\", BadgeVariant::Error);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// empty_state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_empty_state() {
|
||||
demo_header("empty_state", "v1.0.0",
|
||||
"Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias.");
|
||||
|
||||
ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders);
|
||||
empty_state("( no data )", "No projects yet",
|
||||
"Create one under projects/{name}/ with project.md and reindex");
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"if (apps.empty()) {\n"
|
||||
" empty_state(\"( no data )\", \"No apps yet\",\n"
|
||||
" \"Click + Add to create one\");\n"
|
||||
" return;\n"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// page_header
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_page_header() {
|
||||
demo_header("page_header", "v1.0.0",
|
||||
"Header de pagina con titulo, subtitulo opcional y separador final. "
|
||||
"Patron begin/end permite insertar acciones entre titulo y separador.");
|
||||
|
||||
page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses");
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f);
|
||||
toolbar_begin();
|
||||
button("Reload", V::Subtle); ImGui::SameLine();
|
||||
button("+ Add", V::Secondary);
|
||||
toolbar_end();
|
||||
page_header_end();
|
||||
|
||||
code_block(
|
||||
"page_header_begin(\"Dashboard\", subtitle);\n"
|
||||
"ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n"
|
||||
"toolbar_begin();\n"
|
||||
" button(\"Reload\", Subtle);\n"
|
||||
"toolbar_end();\n"
|
||||
"page_header_end();"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// dashboard_panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_dashboard_panel() {
|
||||
demo_header("dashboard_panel", "v1.0.0",
|
||||
"Contenedor tipo panel con titulo, bordes redondeados, bg surface. "
|
||||
"Auto-resize-Y segun contenido. Usa min_width/min_height como piso.");
|
||||
|
||||
if (dashboard_panel_begin("Revenue", 0, 120.0f)) {
|
||||
ImGui::Text("Some panel content goes here.");
|
||||
ImGui::Text("Anything drawn inside lives in the child window.");
|
||||
}
|
||||
dashboard_panel_end();
|
||||
|
||||
code_block(
|
||||
"if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n"
|
||||
" ImGui::Text(\"content\");\n"
|
||||
"}\n"
|
||||
"dashboard_panel_end();"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,215 +0,0 @@
|
||||
// Demos faltantes: process_runner (Core), candlestick / gauge / heatmap /
|
||||
// table_view (Viz). Aniade cobertura sobre los primitivos del registry que
|
||||
// no tenian su entry en la gallery.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "core/process_runner.h"
|
||||
#include "viz/candlestick.h"
|
||||
#include "viz/gauge.h"
|
||||
#include "viz/heatmap.h"
|
||||
#include "viz/table_view.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// process_runner (Core)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_process_runner() {
|
||||
demo_header("process_runner", "v1.0.0",
|
||||
"Ejecuta una tarea en std::thread en background y expone estado thread-safe "
|
||||
"(idle/running/success/error). El widget runner_status() dibuja inline un "
|
||||
"spinner mientras corre y un mensaje de Success/Error al terminar.");
|
||||
|
||||
static fn_ui::ProcessRunner runner;
|
||||
|
||||
section("Tarea simulada (sleep 2s)");
|
||||
{
|
||||
if (ImGui::Button("Run task")) {
|
||||
if (!runner.is_busy()) {
|
||||
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
out = "task done in 2s";
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Run failing task")) {
|
||||
if (!runner.is_busy()) {
|
||||
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
out = "simulated failure";
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset")) runner.reset();
|
||||
fn_ui::runner_status(runner, "Working...");
|
||||
}
|
||||
|
||||
code_block(
|
||||
"static fn_ui::ProcessRunner r;\n"
|
||||
"if (button(\"Run\", Primary) && !r.is_busy()) {\n"
|
||||
" fn_ui::runner_trigger(r, [](std::string& out) -> bool {\n"
|
||||
" return do_work(&out);\n"
|
||||
" });\n"
|
||||
"}\n"
|
||||
"fn_ui::runner_status(r, \"Working...\");"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// candlestick (Viz)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_candlestick() {
|
||||
demo_header("candlestick", "v1.0.0",
|
||||
"Grafico de velas OHLC con ImPlot custom rendering. Verde si close >= open, "
|
||||
"rojo si bajista. Tooltip al hover muestra OHLC del dia.");
|
||||
|
||||
section("OHLC sintetico (30 dias)");
|
||||
{
|
||||
static std::vector<double> dates, opens, closes, lows, highs;
|
||||
if (dates.empty()) {
|
||||
dates.reserve(30); opens.reserve(30); closes.reserve(30);
|
||||
lows.reserve(30); highs.reserve(30);
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
double drift = std::sin(i * 0.4) * 1.2;
|
||||
double o = price;
|
||||
double c = price + drift + (i % 3 == 0 ? -0.6 : 0.4);
|
||||
double l = std::min(o, c) - 0.8 - (i % 5) * 0.1;
|
||||
double h = std::max(o, c) + 0.8 + (i % 4) * 0.1;
|
||||
dates.push_back(double(i));
|
||||
opens.push_back(o);
|
||||
closes.push_back(c);
|
||||
lows.push_back(l);
|
||||
highs.push_back(h);
|
||||
price = c;
|
||||
}
|
||||
}
|
||||
candlestick("##cs", dates.data(), opens.data(), closes.data(),
|
||||
lows.data(), highs.data(), int(dates.size()));
|
||||
}
|
||||
|
||||
code_block(
|
||||
"candlestick(\"##cs\", dates, opens, closes, lows, highs, n,\n"
|
||||
" /*width_percent=*/0.25f, /*tooltip=*/true);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// gauge (Viz)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_gauge() {
|
||||
demo_header("gauge", "v1.0.0",
|
||||
"Indicador circular tipo velocimetro con ImGui DrawList. Color interpolado "
|
||||
"verde -> amarillo -> rojo segun el valor normalizado.");
|
||||
|
||||
static float v_cpu = 0.32f, v_mem = 0.78f, v_gpu = 0.55f;
|
||||
|
||||
section("Tres gauges con sliders");
|
||||
{
|
||||
ImGui::SliderFloat("cpu", &v_cpu, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("mem", &v_mem, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("gpu", &v_gpu, 0.0f, 1.0f);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::BeginGroup();
|
||||
gauge("CPU", v_cpu, 0.0f, 1.0f, 60.0f);
|
||||
ImGui::EndGroup();
|
||||
ImGui::SameLine(0.0f, 24.0f);
|
||||
ImGui::BeginGroup();
|
||||
gauge("MEM", v_mem, 0.0f, 1.0f, 60.0f);
|
||||
ImGui::EndGroup();
|
||||
ImGui::SameLine(0.0f, 24.0f);
|
||||
ImGui::BeginGroup();
|
||||
gauge("GPU", v_gpu, 0.0f, 1.0f, 60.0f);
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
|
||||
code_block("gauge(\"CPU\", 0.32f, 0.0f, 1.0f, 60.0f);");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// heatmap (Viz)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_heatmap() {
|
||||
demo_header("heatmap", "v1.0.0",
|
||||
"Mapa de calor 2D con ImPlot. Datos row-major. Util para correlation "
|
||||
"matrices, attention maps, distribuciones 2D discretas.");
|
||||
|
||||
constexpr int R = 12;
|
||||
constexpr int C = 12;
|
||||
static float values[R * C] = {0};
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
for (int r = 0; r < R; ++r) {
|
||||
for (int c = 0; c < C; ++c) {
|
||||
float dx = (c - C * 0.5f) / float(C);
|
||||
float dy = (r - R * 0.5f) / float(R);
|
||||
values[r * C + c] = std::exp(-(dx * dx + dy * dy) * 6.0f);
|
||||
}
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
|
||||
section("Gaussian 12x12");
|
||||
{
|
||||
heatmap("##hm", values, R, C, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
code_block(
|
||||
"float values[R * C];\n"
|
||||
"// fill row-major: values[r * C + c] = ...\n"
|
||||
"heatmap(\"##hm\", values, R, C, /*min=*/0.0f, /*max=*/1.0f);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// table_view (Viz)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_table_view() {
|
||||
demo_header("table_view", "v1.0.0",
|
||||
"Tabla interactiva con sorting indicators y scroll usando la ImGui Tables API. "
|
||||
"Headers + cells row-major. Util para dashboards y inspectores.");
|
||||
|
||||
section("Lenguajes del registry");
|
||||
{
|
||||
const char* headers[] = {"id", "lang", "domain", "purity"};
|
||||
// 6 filas x 4 cols, row-major
|
||||
const char* cells[] = {
|
||||
"filter_slice_go_core", "go", "core", "pure",
|
||||
"metabase_setup_py_infra", "py", "infra", "impure",
|
||||
"rsync_deploy_bash_infra", "sh", "infra", "impure",
|
||||
"button_cpp_core", "cpp", "core", "pure",
|
||||
"gl_texture_load_cpp_gfx", "cpp", "gfx", "impure",
|
||||
"audio_fft_cpp_core", "cpp", "core", "pure",
|
||||
};
|
||||
const int row_count = 6;
|
||||
const int col_count = 4;
|
||||
table_view("##tbl", headers, col_count, cells, row_count);
|
||||
}
|
||||
|
||||
code_block(
|
||||
"const char* headers[] = {\"id\", \"lang\", \"domain\"};\n"
|
||||
"const char* cells[] = {/* row-major: r0c0,r0c1,r0c2, r1c0,... */};\n"
|
||||
"table_view(\"##tbl\", headers, 3, cells, n_rows);"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,196 +0,0 @@
|
||||
// Demos del dominio gfx — primitivos OpenGL/shader que viven en
|
||||
// cpp/functions/gfx/. La pieza distintiva de shaders_lab es el
|
||||
// shader_canvas: framebuffer + fullscreen quad + programa GL animado por
|
||||
// time/resolution/mouse.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "gfx/shader_canvas.h"
|
||||
#include "gfx/gl_shader.h"
|
||||
#include "gfx/gl_loader.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <chrono>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
namespace {
|
||||
|
||||
// Fragment shader sintetico — gradiente animado con celdas. Usa los uniforms
|
||||
// estandar que compile_fragment inyecta: u_resolution, u_time, u_mouse.
|
||||
const char* kShaderSrc = R"(
|
||||
void mainImage() {
|
||||
vec2 uv = gl_FragCoord.xy / u_resolution;
|
||||
vec2 cell = uv * 8.0;
|
||||
vec2 ipos = floor(cell);
|
||||
vec2 fpos = fract(cell) - 0.5;
|
||||
|
||||
float t = u_time * 0.6;
|
||||
float wave = sin(ipos.x * 0.7 + ipos.y * 0.5 + t);
|
||||
float dist = length(fpos);
|
||||
|
||||
vec3 a = vec3(0.30, 0.43, 0.96); // indigo
|
||||
vec3 b = vec3(0.95, 0.45, 0.85); // pink
|
||||
vec3 col = mix(a, b, 0.5 + 0.5 * wave);
|
||||
|
||||
// Mouse focus: oscurecemos celdas lejanas al cursor.
|
||||
vec2 m = u_mouse / u_resolution;
|
||||
float fm = 1.0 - smoothstep(0.0, 0.6, length(uv - m));
|
||||
col *= 0.6 + 0.4 * fm;
|
||||
|
||||
// Disco interior por celda con borde suave.
|
||||
col *= smoothstep(0.5, 0.45, dist);
|
||||
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
mainImage();
|
||||
}
|
||||
)";
|
||||
|
||||
struct CanvasState {
|
||||
fn::gfx::ShaderCanvas canvas;
|
||||
bool compiled = false;
|
||||
bool compile_failed = false;
|
||||
std::string err_msg;
|
||||
std::chrono::steady_clock::time_point t0;
|
||||
};
|
||||
|
||||
CanvasState& state() {
|
||||
static CanvasState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_shader_canvas() {
|
||||
demo_header("shader_canvas", "v1.0.0",
|
||||
"Framebuffer + fullscreen quad + shader GLSL animado. La misma pieza "
|
||||
"que usa shaders_lab para el preview en vivo. Uniforms u_time / u_resolution / u_mouse "
|
||||
"los inyecta gl_shader::compile_fragment automaticamente.");
|
||||
|
||||
auto& s = state();
|
||||
|
||||
// Compilacion lazy (en el primer frame ya hay contexto GL valido).
|
||||
if (!s.compiled && !s.compile_failed) {
|
||||
fn::gfx::gl_loader_init();
|
||||
fn::gfx::canvas_init(s.canvas);
|
||||
|
||||
auto cr = fn::gfx::compile_fragment(kShaderSrc);
|
||||
if (!cr.ok) {
|
||||
s.compile_failed = true;
|
||||
s.err_msg = cr.err_msg;
|
||||
} else {
|
||||
fn::gfx::canvas_set_program(s.canvas, cr.program);
|
||||
s.t0 = std::chrono::steady_clock::now();
|
||||
s.compiled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (s.compile_failed) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1),
|
||||
"Compilacion del fragment shader fallo:\n%s",
|
||||
s.err_msg.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
section("Live preview");
|
||||
|
||||
// Render del shader en un panel ~480x300 px. canvas_render hace resize
|
||||
// automatico segun GetContentRegionAvail si lo dejas crecer.
|
||||
ImGui::BeginChild("##shader_preview", ImVec2(480, 300),
|
||||
ImGuiChildFlags_Borders);
|
||||
const float dt = std::chrono::duration<float>(
|
||||
std::chrono::steady_clock::now() - s.t0).count();
|
||||
fn::gfx::canvas_render(s.canvas, dt);
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"#include \"gfx/shader_canvas.h\"\n"
|
||||
"#include \"gfx/gl_shader.h\"\n\n"
|
||||
"static fn::gfx::ShaderCanvas canvas;\n"
|
||||
"// Setup (una vez):\n"
|
||||
"fn::gfx::canvas_init(canvas);\n"
|
||||
"auto cr = fn::gfx::compile_fragment(user_glsl);\n"
|
||||
"if (cr.ok) fn::gfx::canvas_set_program(canvas, cr.program);\n\n"
|
||||
"// Cada frame, dentro de un Begin/End:\n"
|
||||
"fn::gfx::canvas_render(canvas, time_seconds);"
|
||||
);
|
||||
}
|
||||
|
||||
// Issue 0049b — Mostrar la version de OpenGL del contexto y un puñado de
|
||||
// limites 4.3 que confirman que compute shaders / SSBO / image load-store
|
||||
// estan disponibles. No es codigo del registry, solo introspeccion del
|
||||
// driver — sin estado, sin side effects: solo glGetString + glGetIntegerv.
|
||||
|
||||
#ifndef GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
|
||||
#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD
|
||||
#endif
|
||||
#ifndef GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS
|
||||
#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC
|
||||
#endif
|
||||
#ifndef GL_MAX_COMPUTE_SHARED_MEMORY_SIZE
|
||||
#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
|
||||
#endif
|
||||
|
||||
void demo_gl_info() {
|
||||
demo_header("gl_info", "v1.0.0",
|
||||
"Introspeccion del contexto OpenGL activo (issue 0049b). El framework "
|
||||
"ahora pide GL 4.3 core, lo que habilita compute shaders, SSBOs, image "
|
||||
"load/store y atomic counters — bloques esenciales del graph_renderer "
|
||||
"GPU del proyecto osint_graph.");
|
||||
|
||||
auto gl_str = [](GLenum e) -> const char* {
|
||||
const GLubyte* s = glGetString(e);
|
||||
return s ? reinterpret_cast<const char*>(s) : "(null)";
|
||||
};
|
||||
|
||||
section("Driver");
|
||||
ImGui::Text("Vendor: %s", gl_str(GL_VENDOR));
|
||||
ImGui::Text("Renderer: %s", gl_str(GL_RENDERER));
|
||||
ImGui::Text("Version: %s", gl_str(GL_VERSION));
|
||||
ImGui::Text("GLSL: %s", gl_str(GL_SHADING_LANGUAGE_VERSION));
|
||||
|
||||
GLint major = 0, minor = 0;
|
||||
glGetIntegerv(GL_MAJOR_VERSION, &major);
|
||||
glGetIntegerv(GL_MINOR_VERSION, &minor);
|
||||
|
||||
const bool has_43 = (major > 4) || (major == 4 && minor >= 3);
|
||||
section("Capabilities");
|
||||
ImGui::Text("Context: %d.%d core", major, minor);
|
||||
if (has_43) {
|
||||
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f),
|
||||
"OpenGL 4.3+ — compute shaders, SSBOs, image load/store, atomic counters: AVAILABLE");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.30f, 1.0f),
|
||||
"OpenGL < 4.3 — compute shaders / SSBOs NOT available on this driver");
|
||||
}
|
||||
|
||||
section("Limits");
|
||||
GLint v = 0;
|
||||
auto row = [&](const char* label, GLenum e) {
|
||||
v = 0;
|
||||
glGetIntegerv(e, &v);
|
||||
ImGui::Text("%-44s %d", label, v);
|
||||
};
|
||||
row("GL_MAX_TEXTURE_SIZE", GL_MAX_TEXTURE_SIZE);
|
||||
row("GL_MAX_VERTEX_ATTRIBS", GL_MAX_VERTEX_ATTRIBS);
|
||||
row("GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BLOCK_SIZE);
|
||||
if (has_43) {
|
||||
row("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
|
||||
row("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS);
|
||||
row("GL_MAX_COMPUTE_SHARED_MEMORY_SIZE", GL_MAX_COMPUTE_SHARED_MEMORY_SIZE);
|
||||
}
|
||||
|
||||
code_block(
|
||||
"// Solo glGetString + glGetIntegerv — sin loader extra.\n"
|
||||
"GLint major = 0, minor = 0;\n"
|
||||
"glGetIntegerv(GL_MAJOR_VERSION, &major);\n"
|
||||
"glGetIntegerv(GL_MINOR_VERSION, &minor);\n"
|
||||
"bool has_compute = (major > 4) || (major == 4 && minor >= 3);"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,127 +0,0 @@
|
||||
// Demo de gl_texture_load (cpp/functions/gfx/gl_texture_load.{h,cpp}).
|
||||
// Carga assets/sample.png y lo muestra con ImGui::Image. Sliders para tint
|
||||
// RGB que se aplican como modulacion (ImGui::Image acepta tint_col).
|
||||
//
|
||||
// Limitacion: el "zoom UV" se simula moviendo uv0/uv1 (que ImGui::Image acepta
|
||||
// nativamente). Asi evitamos compilar un shader custom adicional para la demo.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "gfx/gl_texture_load.h"
|
||||
#include "gfx/gl_loader.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
namespace {
|
||||
|
||||
struct TextureState {
|
||||
fn::GlTexture tex{};
|
||||
bool tried_load = false;
|
||||
std::string_view err;
|
||||
char err_buf[256] = {0};
|
||||
float tint[3] = {1.0f, 1.0f, 1.0f};
|
||||
float zoom = 1.0f; // 1.0 = sin zoom; >1 hace UV mas pequeno
|
||||
};
|
||||
|
||||
TextureState& state() {
|
||||
static TextureState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Resuelve un path para el asset. Probamos varios candidatos relativos al cwd
|
||||
// del binario (puede lanzarse desde build/ o desde la raiz del repo).
|
||||
const char* resolve_sample_path() {
|
||||
static const char* candidates[] = {
|
||||
"assets/sample.png",
|
||||
"apps/primitives_gallery/assets/sample.png",
|
||||
"cpp/apps/primitives_gallery/assets/sample.png",
|
||||
"../cpp/apps/primitives_gallery/assets/sample.png",
|
||||
"../../cpp/apps/primitives_gallery/assets/sample.png",
|
||||
"../../../cpp/apps/primitives_gallery/assets/sample.png",
|
||||
nullptr,
|
||||
};
|
||||
for (int i = 0; candidates[i]; i++) {
|
||||
FILE* f = std::fopen(candidates[i], "rb");
|
||||
if (f) { std::fclose(f); return candidates[i]; }
|
||||
}
|
||||
return candidates[0]; // devolver el primer candidato para que el error sea mas descriptivo
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_gl_texture() {
|
||||
demo_header("gl_texture_load", "v1.0.0",
|
||||
"Carga PNG/JPG/HDR desde disco a una textura GL lista para sampler2D. "
|
||||
"Vendorea stb_image (cpp/vendor/stb/). Demo: assets/sample.png "
|
||||
"(damero 256x256), tint RGB modulando ImGui::Image, zoom UV.");
|
||||
|
||||
auto& s = state();
|
||||
|
||||
if (!s.tried_load) {
|
||||
// Asegurar simbolos GL resueltos (Linux no-op, Windows wglGetProcAddress).
|
||||
fn::gfx::gl_loader_init();
|
||||
const char* path = resolve_sample_path();
|
||||
s.tex = fn::gl_texture_load(path, /*flip_y=*/true, /*srgb=*/false);
|
||||
if (!s.tex.ok()) {
|
||||
std::snprintf(s.err_buf, sizeof(s.err_buf),
|
||||
"no se pudo cargar '%s': %s",
|
||||
path, fn::gl_texture_last_error());
|
||||
}
|
||||
s.tried_load = true;
|
||||
}
|
||||
|
||||
if (!s.tex.ok()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", s.err_buf);
|
||||
ImGui::TextWrapped(
|
||||
"El binario busca el PNG en varios paths relativos al cwd. "
|
||||
"Lanzar desde la raiz del repo o desde cpp/build/ deberia funcionar.");
|
||||
return;
|
||||
}
|
||||
|
||||
section("Texture info");
|
||||
ImGui::Text("size: %d x %d px", s.tex.w, s.tex.h);
|
||||
ImGui::Text("channels: %d (forzado a RGBA en upload)", s.tex.channels);
|
||||
ImGui::Text("gl_id: %u", (unsigned)s.tex.id);
|
||||
|
||||
section("Tint + zoom");
|
||||
ImGui::SliderFloat3("tint RGB", s.tint, 0.0f, 2.0f, "%.2f");
|
||||
ImGui::SliderFloat("zoom UV", &s.zoom, 0.25f, 4.0f, "%.2fx");
|
||||
|
||||
section("Preview");
|
||||
|
||||
// Calcular UVs centradas con zoom: 1.0 = (0,0)-(1,1), 2.0 = (0.25,0.25)-(0.75,0.75)
|
||||
float u_half = 0.5f / (s.zoom > 0.001f ? s.zoom : 0.001f);
|
||||
ImVec2 uv0(0.5f - u_half, 0.5f - u_half);
|
||||
ImVec2 uv1(0.5f + u_half, 0.5f + u_half);
|
||||
|
||||
ImVec4 tint(s.tint[0], s.tint[1], s.tint[2], 1.0f);
|
||||
|
||||
// Conversion GLuint -> ImTextureID. ImGui::Image acepta cualquier id de
|
||||
// textura del backend; en imgui_impl_opengl3 es directamente el GLuint.
|
||||
ImTextureID tid = (ImTextureID)(intptr_t)s.tex.id;
|
||||
|
||||
ImGui::ImageWithBg(tid, ImVec2(384.0f, 384.0f), uv0, uv1,
|
||||
ImVec4(0, 0, 0, 0), tint);
|
||||
|
||||
code_block(
|
||||
"#include \"gfx/gl_texture_load.h\"\n\n"
|
||||
"auto tex = fn::gl_texture_load(\"assets/sample.png\");\n"
|
||||
"if (!tex.ok()) {\n"
|
||||
" fprintf(stderr, \"%s\\n\", fn::gl_texture_last_error());\n"
|
||||
" return 1;\n"
|
||||
"}\n"
|
||||
"// uso en shader:\n"
|
||||
"glUseProgram(prog);\n"
|
||||
"fn::gl_texture_bind_uniform(prog, \"u_tex\", tex, /*unit=*/0);\n"
|
||||
"glDrawArrays(GL_TRIANGLES, 0, 6);\n\n"
|
||||
"// o en ImGui directamente:\n"
|
||||
"ImGui::Image((ImTextureID)(intptr_t)tex.id, ImVec2(w, h));"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,443 +0,0 @@
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/graph_types.h"
|
||||
#include "viz/graph_viewport.h"
|
||||
#include "viz/graph_force_layout.h"
|
||||
#include "viz/graph_force_layout_gpu.h"
|
||||
#include "viz/graph_layouts.h"
|
||||
#include "viz/graph_labels.h"
|
||||
#include "core/button.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la
|
||||
// tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo
|
||||
// queda demostrado tal cual lo van a usar las apps reales (osint_graph,
|
||||
// fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id.
|
||||
static const uint32_t k_demo_palette[] = {
|
||||
0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u,
|
||||
0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u,
|
||||
};
|
||||
static constexpr int k_demo_palette_n =
|
||||
sizeof(k_demo_palette) / sizeof(k_demo_palette[0]);
|
||||
|
||||
// Tabla compartida entre regeneraciones — las apariencias no cambian aunque
|
||||
// el usuario regenere el grafo, asi que vive como `static`.
|
||||
static EntityType s_demo_entity_types[k_demo_palette_n];
|
||||
static RelationType s_demo_relation_types[1];
|
||||
static bool s_demo_types_initialized = false;
|
||||
|
||||
static void init_demo_types() {
|
||||
if (s_demo_types_initialized) return;
|
||||
for (int k = 0; k < k_demo_palette_n; ++k) {
|
||||
s_demo_entity_types[k] = entity_type(k_demo_palette[k],
|
||||
SHAPE_CIRCLE, 4.0f, "cluster");
|
||||
}
|
||||
s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default");
|
||||
s_demo_types_initialized = true;
|
||||
}
|
||||
|
||||
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
|
||||
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
|
||||
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
|
||||
// mismo cuadrado de 200 px — a 1M nodos crece a ~6 km de radio en graph
|
||||
// space y los nodos pueden esparcirse libremente sin caja artificial.
|
||||
static void generate_synthetic_graph(int N, int K,
|
||||
int edges_per_node, int inter_pct,
|
||||
std::vector<GraphNode>& nodes_out,
|
||||
std::vector<GraphEdge>& edges_out) {
|
||||
nodes_out.clear();
|
||||
edges_out.clear();
|
||||
nodes_out.reserve(N);
|
||||
edges_out.reserve((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u);
|
||||
|
||||
unsigned seed = 0x1234abcd;
|
||||
auto rnd = [&]() {
|
||||
seed = seed * 1664525u + 1013904223u;
|
||||
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
|
||||
};
|
||||
|
||||
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
|
||||
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
|
||||
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
|
||||
const float scale = std::sqrt(static_cast<float>(std::max(N, 1)));
|
||||
const float cluster_r = 12.0f * scale;
|
||||
const float scatter = 4.0f * scale;
|
||||
|
||||
std::vector<float> cluster_cx(K), cluster_cy(K);
|
||||
for (int k = 0; k < K; k++) {
|
||||
float angle = 2.0f * 3.14159f * k / K;
|
||||
cluster_cx[k] = std::cos(angle) * cluster_r;
|
||||
cluster_cy[k] = std::sin(angle) * cluster_r;
|
||||
}
|
||||
|
||||
for (int i = 0; i < N; i++) {
|
||||
int k = i % K;
|
||||
// type_id mapea al EntityType (k % k_demo_palette_n) que define
|
||||
// color y shape. size_override = 3..5 px para conservar la
|
||||
// variacion sutil del demo v1 — apariencia visual identica.
|
||||
uint16_t tid = static_cast<uint16_t>(k % k_demo_palette_n);
|
||||
GraphNode n = graph_node(
|
||||
cluster_cx[k] + (rnd() - 0.5f) * scatter,
|
||||
cluster_cy[k] + (rnd() - 0.5f) * scatter,
|
||||
tid);
|
||||
n.size_override = 3.0f + rnd() * 2.0f;
|
||||
n.user_data = static_cast<uint64_t>(i);
|
||||
nodes_out.push_back(n);
|
||||
}
|
||||
|
||||
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
|
||||
if (a == b) return;
|
||||
edges_out.push_back(graph_edge(a, b, w));
|
||||
};
|
||||
int per_cluster = N / K;
|
||||
for (int k = 0; k < K; k++) {
|
||||
int base = k * per_cluster;
|
||||
int end = (k == K - 1) ? N : (base + per_cluster);
|
||||
int size = end - base;
|
||||
if (size < 2) continue;
|
||||
for (int i = base; i < end; i++) {
|
||||
for (int e = 0; e < edges_per_node; e++) {
|
||||
int j = base + static_cast<int>(rnd() * size);
|
||||
add_edge(static_cast<uint32_t>(i),
|
||||
static_cast<uint32_t>(j), 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Inter-cluster: pct% del total de nodos
|
||||
long long inter = (long long)N * (long long)inter_pct / 100LL;
|
||||
for (long long e = 0; e < inter; e++) {
|
||||
uint32_t a = static_cast<uint32_t>(rnd() * N);
|
||||
uint32_t b = static_cast<uint32_t>(rnd() * N);
|
||||
add_edge(a, b, 0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
void demo_graph() {
|
||||
demo_header("graph_viewport", "v1.0.0",
|
||||
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
|
||||
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
|
||||
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
|
||||
|
||||
static int s_n_nodes = 1000;
|
||||
static int s_n_clusters = 6;
|
||||
static int s_edges_per_n = 3; // aristas intra-cluster por nodo
|
||||
static int s_inter_pct = 5; // % de nodos para edges inter-cluster
|
||||
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
|
||||
static float s_attraction = 0.02f; // muelle entre nodos conectados
|
||||
static float s_gravity = 0.001f; // tiron hacia el centro
|
||||
static std::vector<GraphNode> s_nodes;
|
||||
static std::vector<GraphEdge> s_edges;
|
||||
static GraphData s_graph{};
|
||||
static GraphViewportState s_state;
|
||||
static bool s_initialized = false;
|
||||
static bool s_needs_regen = true;
|
||||
|
||||
// GPU layout (issue 0049h): toggle CPU/GPU. ctx se crea perezosamente al
|
||||
// primer frame en GPU mode; max_nodes/max_edges se dimensionan al maximo
|
||||
// que ofrece el slider (1M nodos x 10 edges/nodo = 10M edges) — los SSBOs
|
||||
// ocupan ~80 MB en ese tope, suficientemente barato para no
|
||||
// recrear el ctx cada Regenerate. Si compute no esta disponible, el
|
||||
// toggle queda deshabilitado.
|
||||
static bool s_use_gpu = false;
|
||||
static ForceLayoutGPU* s_gpu_ctx = nullptr;
|
||||
static bool s_gpu_dirty = true; // re-upload tras regen / cambio
|
||||
|
||||
// Layout estatico activo (issue 0049i). 0=force (iterativo), 1=grid,
|
||||
// 2=circular, 3=radial, 4=hierarchical, 5=fixed.
|
||||
static int s_layout_mode = 0;
|
||||
const char* k_layout_names[] = {
|
||||
"force", "grid", "circular", "radial", "hierarchical", "fixed"
|
||||
};
|
||||
static int s_apply_layout = 0; // se incrementa cuando hay que reaplicar
|
||||
|
||||
// Labels (issue 0049j). LabelPolicy controlable desde la UI.
|
||||
static graph::LabelPolicy s_label_policy;
|
||||
static bool s_labels_enabled = true;
|
||||
|
||||
if (s_needs_regen) {
|
||||
init_demo_types();
|
||||
generate_synthetic_graph(s_n_nodes, s_n_clusters,
|
||||
s_edges_per_n, s_inter_pct,
|
||||
s_nodes, s_edges);
|
||||
s_graph.nodes = s_nodes.data();
|
||||
s_graph.node_count = static_cast<int>(s_nodes.size());
|
||||
s_graph.node_capacity = static_cast<int>(s_nodes.capacity());
|
||||
s_graph.edges = s_edges.data();
|
||||
s_graph.edge_count = static_cast<int>(s_edges.size());
|
||||
s_graph.edge_capacity = static_cast<int>(s_edges.capacity());
|
||||
s_graph.types = s_demo_entity_types;
|
||||
s_graph.type_count = k_demo_palette_n;
|
||||
s_graph.rel_types = s_demo_relation_types;
|
||||
s_graph.rel_type_count = 1;
|
||||
s_graph.update_bounds();
|
||||
s_state.layout_running = true;
|
||||
s_state.layout_energy = 0.0f;
|
||||
s_needs_regen = false;
|
||||
s_initialized = true;
|
||||
s_gpu_dirty = true;
|
||||
}
|
||||
|
||||
section("Controls");
|
||||
{
|
||||
using namespace fn_ui;
|
||||
ImGui::PushItemWidth(180);
|
||||
// Slider Nodes con escala logaritmica para que sea util tanto a 100
|
||||
// como a 1M sin tener que arrastrar 10000px.
|
||||
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 1000000, "%d",
|
||||
ImGuiSliderFlags_Logarithmic);
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
|
||||
ImGui::SliderInt("Edges/node", &s_edges_per_n, 1, 10);
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderInt("Inter %", &s_inter_pct, 0, 30, "%d%%");
|
||||
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f");
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true;
|
||||
ImGui::SameLine();
|
||||
if (button(s_state.layout_running ? "Pause layout" : "Resume layout",
|
||||
ButtonVariant::Secondary)) {
|
||||
s_state.layout_running = !s_state.layout_running;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (button("Fit view", ButtonVariant::Subtle)) {
|
||||
graph_viewport_fit(s_graph, s_state);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
// Toggle GPU layout. Si compute no esta disponible (Mesa software o
|
||||
// driver < 4.3), deshabilitamos visualmente el checkbox.
|
||||
bool prev_gpu = s_use_gpu;
|
||||
if (s_gpu_ctx == nullptr && s_use_gpu == false) {
|
||||
// primera oportunidad: intentar crear el ctx para detectar soporte.
|
||||
// Lazy init solo si el usuario lo activa.
|
||||
}
|
||||
ImGui::Checkbox("GPU layout", &s_use_gpu);
|
||||
if (s_use_gpu != prev_gpu) {
|
||||
s_gpu_dirty = true; // re-upload al cambiar de modo
|
||||
}
|
||||
|
||||
// Selector de layout (issue 0049i).
|
||||
ImGui::PushItemWidth(140);
|
||||
int prev_mode = s_layout_mode;
|
||||
if (ImGui::Combo("Layout", &s_layout_mode,
|
||||
k_layout_names, IM_ARRAYSIZE(k_layout_names))) {
|
||||
// Cambio de modo: reaplicar instantaneamente
|
||||
s_apply_layout++;
|
||||
}
|
||||
if (prev_mode != s_layout_mode) {
|
||||
// En "force" volvemos a animar; en cualquier estatico paramos.
|
||||
s_state.layout_running = (s_layout_mode == 0);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::SameLine();
|
||||
if (button("Apply layout", ButtonVariant::Subtle)) s_apply_layout++;
|
||||
|
||||
// --- Labels (issue 0049j) ---------------------------------------
|
||||
ImGui::Checkbox("Labels", &s_labels_enabled);
|
||||
ImGui::SameLine();
|
||||
ImGui::PushItemWidth(140);
|
||||
ImGui::SliderInt("Max visible", &s_label_policy.max_visible, 0, 1000);
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderFloat("Font", &s_label_policy.font_size,
|
||||
8.0f, 24.0f, "%.0f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderFloat("Min px", &s_label_policy.min_node_pixel_size,
|
||||
0.0f, 40.0f, "%.0f");
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Selected", &s_label_policy.always_for_selected);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Hovered", &s_label_policy.always_for_hovered);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Pinned", &s_label_policy.always_for_pinned);
|
||||
}
|
||||
|
||||
section("Stats");
|
||||
{
|
||||
// Una sola linea fija — sin secciones condicionales que cambien la
|
||||
// altura del panel (eso provocaba que el viewport saltara al hacer
|
||||
// hover/select).
|
||||
char hover_buf[32];
|
||||
char sel_buf[32];
|
||||
if (s_state.hovered_node >= 0) {
|
||||
std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u",
|
||||
s_state.hovered_node,
|
||||
(unsigned)s_nodes[s_state.hovered_node].type_id);
|
||||
} else {
|
||||
std::snprintf(hover_buf, sizeof(hover_buf), "-");
|
||||
}
|
||||
if (s_state.selected_node >= 0) {
|
||||
std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node);
|
||||
} else {
|
||||
std::snprintf(sel_buf, sizeof(sel_buf), "-");
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||
ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s",
|
||||
s_graph.node_count, s_graph.edge_count,
|
||||
s_state.layout_energy, ImGui::GetIO().Framerate,
|
||||
hover_buf, sel_buf);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Aplicar layout estatico cuando se solicita (cambio de modo / boton).
|
||||
static int s_last_apply = -1;
|
||||
if (s_apply_layout != s_last_apply) {
|
||||
s_last_apply = s_apply_layout;
|
||||
switch (s_layout_mode) {
|
||||
case 1: graph::layout_grid (s_graph, 25.0f); break;
|
||||
case 2: graph::layout_circular (s_graph, 200.0f); break;
|
||||
case 3: graph::layout_radial (s_graph, 0, 80.0f); break;
|
||||
case 4: graph::layout_hierarchical(s_graph, 0, 120.0f, 50.0f); break;
|
||||
case 5: graph::layout_fixed (s_graph); break;
|
||||
case 0: default:
|
||||
// force: dejar las posiciones actuales; el bucle lo refinara
|
||||
break;
|
||||
}
|
||||
s_gpu_dirty = true;
|
||||
if (s_layout_mode != 0) graph_viewport_fit(s_graph, s_state);
|
||||
}
|
||||
|
||||
section("Viewport (drag=pan, wheel=zoom, click=select, shift+drag=lasso, ctrl+click=toggle)");
|
||||
if (s_initialized) {
|
||||
// Avanzamos 1 paso de force layout cada frame mientras layout_running.
|
||||
// Auto-pause: si la energia por nodo cae bajo el umbral durante N
|
||||
// frames consecutivos, paramos la simulacion automaticamente — el
|
||||
// grafo ya esta estable. El usuario lo retoma con "Resume layout"
|
||||
// o "Regenerate".
|
||||
static int s_low_energy_frames = 0;
|
||||
const int k_pause_after_frames = 30;
|
||||
const float k_pause_per_node = 0.001f; // umbral de energia/nodo
|
||||
if (s_state.layout_running && s_layout_mode == 0) {
|
||||
ForceLayoutConfig cfg;
|
||||
cfg.repulsion = s_repulsion;
|
||||
cfg.attraction = s_attraction;
|
||||
cfg.gravity = s_gravity;
|
||||
cfg.iterations = 1;
|
||||
if (s_use_gpu) {
|
||||
if (!s_gpu_ctx) {
|
||||
s_gpu_ctx = graph_force_layout_gpu_create(s_graph.node_count + 1024,
|
||||
s_graph.edge_count + 1024);
|
||||
s_gpu_dirty = true;
|
||||
}
|
||||
if (s_gpu_ctx) {
|
||||
if (s_gpu_dirty) {
|
||||
graph_force_layout_gpu_upload(s_gpu_ctx, s_graph);
|
||||
s_gpu_dirty = false;
|
||||
}
|
||||
s_state.layout_energy = graph_force_layout_gpu_step(s_gpu_ctx, cfg);
|
||||
graph_force_layout_gpu_readback(s_gpu_ctx, s_graph, /*include_velocities=*/true);
|
||||
} else {
|
||||
// GPU no disponible: caer a CPU silenciosamente.
|
||||
s_use_gpu = false;
|
||||
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
|
||||
}
|
||||
} else {
|
||||
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
|
||||
}
|
||||
|
||||
const float per_node = s_graph.node_count > 0
|
||||
? s_state.layout_energy / (float)s_graph.node_count
|
||||
: 0.0f;
|
||||
if (per_node < k_pause_per_node) ++s_low_energy_frames;
|
||||
else s_low_energy_frames = 0;
|
||||
|
||||
if (graph_force_layout_should_pause(s_low_energy_frames,
|
||||
k_pause_after_frames)) {
|
||||
s_state.layout_running = false;
|
||||
s_low_energy_frames = 0;
|
||||
}
|
||||
} else {
|
||||
s_low_energy_frames = 0;
|
||||
}
|
||||
// Callbacks (issue 0049i): right-click abre popup contextual,
|
||||
// double-click loguea el indice. Los callbacks corren dentro del
|
||||
// frame ImGui — el caller puede usar OpenPopup directamente.
|
||||
static int s_ctx_node = -1;
|
||||
static bool s_ctx_open = false;
|
||||
struct Cb {
|
||||
static void on_ctx(int idx, ImVec2 /*pos*/, void* user) {
|
||||
int* slot = (int*)user;
|
||||
*slot = idx;
|
||||
ImGui::OpenPopup("##graph_node_ctx");
|
||||
}
|
||||
static void on_dbl(int idx, void* /*user*/) {
|
||||
std::printf("[graph] dbl-click on node %d\n", idx);
|
||||
}
|
||||
};
|
||||
GraphViewportCallbacks cb;
|
||||
cb.on_context_menu = &Cb::on_ctx;
|
||||
cb.on_double_click = &Cb::on_dbl;
|
||||
cb.user = &s_ctx_node;
|
||||
|
||||
graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460), cb);
|
||||
|
||||
// Labels overlay (issue 0049j). El callback formatea "#<idx>" en un
|
||||
// buffer estatico por demo — apps reales (osint_graph) usaran un
|
||||
// string pool de la BD origen.
|
||||
if (s_labels_enabled) {
|
||||
struct LblCtx { char buf[32]; };
|
||||
static LblCtx s_lbl_ctx;
|
||||
auto get_label = [](int idx, void* user) -> const char* {
|
||||
auto* ctx = static_cast<LblCtx*>(user);
|
||||
std::snprintf(ctx->buf, sizeof(ctx->buf), "#%d", idx);
|
||||
return ctx->buf;
|
||||
};
|
||||
graph::graph_labels_draw(s_graph, s_state, s_label_policy,
|
||||
get_label, &s_lbl_ctx);
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("##graph_node_ctx")) {
|
||||
ImGui::Text("Node #%d", s_ctx_node);
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Pin")) {
|
||||
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
|
||||
s_graph.nodes[s_ctx_node].flags |= NF_PINNED;
|
||||
}
|
||||
if (ImGui::MenuItem("Unpin")) {
|
||||
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
|
||||
s_graph.nodes[s_ctx_node].flags &= ~NF_PINNED;
|
||||
}
|
||||
if (ImGui::MenuItem("Add to selection")) {
|
||||
graph_viewport_add_to_selection(s_graph, s_state, s_ctx_node);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Overlay con count seleccionados (lasso/multi-select feedback).
|
||||
if (!s_state.selection.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
|
||||
ImGui::Text("[%zu selected]", s_state.selection.size());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
code_block(
|
||||
"static GraphData graph;\n"
|
||||
"static GraphViewportState state;\n"
|
||||
"// ... rellenar graph.nodes / graph.edges ...\n"
|
||||
"graph.update_bounds();\n"
|
||||
"\n"
|
||||
"// Por frame:\n"
|
||||
"if (state.layout_running) {\n"
|
||||
" ForceLayoutConfig cfg;\n"
|
||||
" cfg.repulsion = 3500; cfg.gravity = 0.001f;\n"
|
||||
" graph_force_layout_step(graph, cfg);\n"
|
||||
"}\n"
|
||||
"graph_viewport(\"##g\", graph, state, ImVec2(0, 460));"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,243 +0,0 @@
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/graph_types.h"
|
||||
#include "viz/graph_viewport.h"
|
||||
#include "viz/graph_renderer.h"
|
||||
#include "viz/graph_force_layout.h"
|
||||
#include "viz/graph_icons.h"
|
||||
#include "core/button.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// 6 codepoints Tabler representativos para los 6 EntityTypes del demo. El
|
||||
// orden coincide con `s_entity_types[i]`: cada tipo apunta a `icon_id = i+1`
|
||||
// (las regiones del atlas son 1-indexed; 0 reservado para "sin icono").
|
||||
static const uint16_t k_demo_codepoints[6] = {
|
||||
0xEB4Du, // TI_USER
|
||||
0xEAE5u, // TI_MAIL
|
||||
0xEAB9u, // TI_GLOBE
|
||||
0xEB09u, // TI_PHONE
|
||||
0xEA4Fu, // TI_BUILDING
|
||||
0xEA88u, // TI_DATABASE
|
||||
};
|
||||
|
||||
static const uint32_t k_styles_palette[6] = {
|
||||
0xFF6BCB77u, // verde — Person (circle)
|
||||
0xFFFF6B6Bu, // rojo — Email (square)
|
||||
0xFF4D96FFu, // azul — Domain (diamond)
|
||||
0xFFFFC75Fu, // ambar — Phone (hex)
|
||||
0xFFC780E8u, // morado — Org (triangle)
|
||||
0xFF52CDF2u, // cyan — Database (rounded square)
|
||||
};
|
||||
|
||||
static const char* k_styles_names[6] = {
|
||||
"Person", "Email", "Domain", "Phone", "Organization", "Database"
|
||||
};
|
||||
|
||||
static EntityType s_entity_types[6];
|
||||
static RelationType s_relation_types[3]; // solid, dashed, dotted
|
||||
static IconAtlas* s_atlas = nullptr;
|
||||
static bool s_types_initialized = false;
|
||||
static bool s_atlas_bound = false;
|
||||
|
||||
static void init_demo_types() {
|
||||
if (s_types_initialized) return;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
EntityType t{};
|
||||
t.color = k_styles_palette[i];
|
||||
t.shape = (uint8_t)(SHAPE_CIRCLE + i); // 1..6 — uno por shape
|
||||
t.icon_id = (uint16_t)(i + 1); // 1-based
|
||||
t.default_size = 14.0f;
|
||||
t.name = k_styles_names[i];
|
||||
s_entity_types[i] = t;
|
||||
}
|
||||
s_relation_types[0] = relation_type(0xFFCCCCCCu, EDGE_SOLID, 1.5f, "knows");
|
||||
s_relation_types[1] = relation_type(0xFFFFB870u, EDGE_DASHED, 1.5f, "uses");
|
||||
s_relation_types[2] = relation_type(0xFF89E0FCu, EDGE_DOTTED, 1.5f, "owns");
|
||||
s_types_initialized = true;
|
||||
}
|
||||
|
||||
// 30 nodos posicionados en un anillo por tipo. Aristas: cada nodo conecta a
|
||||
// sus dos vecinos (arc) y a un nodo "central" del cluster siguiente. Mezcla
|
||||
// de directed/undirected para validar las flechas.
|
||||
static void build_demo_graph(std::vector<GraphNode>& nodes,
|
||||
std::vector<GraphEdge>& edges)
|
||||
{
|
||||
nodes.clear();
|
||||
edges.clear();
|
||||
|
||||
const int per_type = 5;
|
||||
const float ring_r = 80.0f;
|
||||
const float type_r = 30.0f;
|
||||
|
||||
for (int t = 0; t < 6; ++t) {
|
||||
float ang_t = (float)t * (2.0f * 3.14159265f / 6.0f);
|
||||
float cx = std::cos(ang_t) * ring_r;
|
||||
float cy = std::sin(ang_t) * ring_r;
|
||||
for (int k = 0; k < per_type; ++k) {
|
||||
float a = (float)k * (2.0f * 3.14159265f / per_type) + ang_t * 0.3f;
|
||||
GraphNode n = graph_node(cx + std::cos(a) * type_r,
|
||||
cy + std::sin(a) * type_r,
|
||||
(uint16_t)t);
|
||||
n.user_data = (uint64_t)nodes.size();
|
||||
nodes.push_back(n);
|
||||
}
|
||||
}
|
||||
|
||||
auto idx = [&](int t, int k) { return (uint32_t)(t * per_type + k); };
|
||||
|
||||
for (int t = 0; t < 6; ++t) {
|
||||
// Aristas intra-cluster (knows = solid, undirected).
|
||||
for (int k = 0; k < per_type; ++k) {
|
||||
int next_k = (k + 1) % per_type;
|
||||
GraphEdge e = graph_edge(idx(t, k), idx(t, next_k), 1.0f, /*type_id=*/0);
|
||||
edges.push_back(e);
|
||||
}
|
||||
// Inter-cluster: del nodo 0 del cluster t al nodo 0 del cluster t+1
|
||||
// como "uses" (dashed, directed).
|
||||
int t_next = (t + 1) % 6;
|
||||
GraphEdge e1 = graph_edge(idx(t, 0), idx(t_next, 0), 1.0f, /*type_id=*/1);
|
||||
e1.flags |= EF_DIRECTED;
|
||||
edges.push_back(e1);
|
||||
|
||||
// Y otra inter-cluster mas larga al cluster +2 como "owns" (dotted,
|
||||
// directed). Asi se ven las 3 estilos a la vez.
|
||||
int t_far = (t + 2) % 6;
|
||||
GraphEdge e2 = graph_edge(idx(t, 2), idx(t_far, 3), 0.6f, /*type_id=*/2);
|
||||
e2.flags |= EF_DIRECTED;
|
||||
edges.push_back(e2);
|
||||
}
|
||||
}
|
||||
|
||||
void demo_graph_styles() {
|
||||
demo_header("graph_renderer (shapes + icons + arrows + edge styles)", "v1.5.0",
|
||||
"OSINT-style: 6 EntityTypes, uno por shape (circle, square, diamond, hex, "
|
||||
"triangle, rounded square) con icono Tabler en el centro. 3 RelationTypes "
|
||||
"(solid/dashed/dotted) con flechas en los aristas EF_DIRECTED. Mismas dos "
|
||||
"draw calls que el viewport normal (1 nodos + 1 aristas).");
|
||||
|
||||
init_demo_types();
|
||||
|
||||
static std::vector<GraphNode> s_nodes;
|
||||
static std::vector<GraphEdge> s_edges;
|
||||
static GraphData s_graph{};
|
||||
static GraphViewportState s_state;
|
||||
static bool s_initialized = false;
|
||||
static bool s_run_layout = false;
|
||||
|
||||
if (!s_initialized) {
|
||||
build_demo_graph(s_nodes, s_edges);
|
||||
s_graph.nodes = s_nodes.data();
|
||||
s_graph.node_count = (int)s_nodes.size();
|
||||
s_graph.node_capacity = (int)s_nodes.capacity();
|
||||
s_graph.edges = s_edges.data();
|
||||
s_graph.edge_count = (int)s_edges.size();
|
||||
s_graph.edge_capacity = (int)s_edges.capacity();
|
||||
s_graph.types = s_entity_types;
|
||||
s_graph.type_count = 6;
|
||||
s_graph.rel_types = s_relation_types;
|
||||
s_graph.rel_type_count = 3;
|
||||
s_graph.update_bounds();
|
||||
s_state.layout_running = false; // queremos ver las shapes posicionadas, no el caos del force
|
||||
s_state.zoom = 2.0f;
|
||||
s_initialized = true;
|
||||
}
|
||||
|
||||
section("Legend");
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
ImGui::Text("%-13s shape=%d icon_id=%d color=#%06x",
|
||||
k_styles_names[i],
|
||||
(int)s_entity_types[i].shape,
|
||||
(int)s_entity_types[i].icon_id,
|
||||
(unsigned)(s_entity_types[i].color & 0x00FFFFFFu));
|
||||
}
|
||||
ImGui::Text("Edges: knows=solid, uses=dashed (directed), owns=dotted (directed)");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
section("Controls");
|
||||
{
|
||||
using namespace fn_ui;
|
||||
if (button(s_run_layout ? "Pause force layout" : "Run force layout",
|
||||
ButtonVariant::Secondary)) {
|
||||
s_run_layout = !s_run_layout;
|
||||
s_state.layout_running = s_run_layout;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (button("Rebuild", ButtonVariant::Subtle)) {
|
||||
build_demo_graph(s_nodes, s_edges);
|
||||
s_graph.nodes = s_nodes.data();
|
||||
s_graph.node_count = (int)s_nodes.size();
|
||||
s_graph.edges = s_edges.data();
|
||||
s_graph.edge_count = (int)s_edges.size();
|
||||
s_graph.update_bounds();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (button("Fit view", ButtonVariant::Subtle)) {
|
||||
graph_viewport_fit(s_graph, s_state);
|
||||
}
|
||||
}
|
||||
|
||||
section("Viewport");
|
||||
if (s_run_layout) {
|
||||
ForceLayoutConfig cfg;
|
||||
cfg.repulsion = 1500.0f;
|
||||
cfg.attraction = 0.04f;
|
||||
cfg.gravity = 0.005f;
|
||||
cfg.iterations = 1;
|
||||
graph_force_layout_step(s_graph, cfg);
|
||||
}
|
||||
|
||||
// El viewport crea internamente el GraphRenderer. La primera vez que se
|
||||
// dibuja el panel, el renderer existe — bindeamos el atlas justo despues.
|
||||
graph_viewport("##graph_styles", s_graph, s_state, ImVec2(0, 460));
|
||||
|
||||
if (!s_atlas_bound && s_state.renderer) {
|
||||
s_atlas = graph_icons_build(k_demo_codepoints, 6, 32);
|
||||
if (s_atlas) {
|
||||
graph_renderer_set_icon_atlas(s_state.renderer,
|
||||
graph_icons_texture(s_atlas),
|
||||
graph_icons_uv_table(s_atlas),
|
||||
graph_icons_count(s_atlas));
|
||||
s_atlas_bound = true;
|
||||
} else {
|
||||
// Sin atlas: marcamos como bound para no reintentar cada frame —
|
||||
// el renderer simplemente pinta sin overlay de iconos.
|
||||
s_atlas_bound = true;
|
||||
}
|
||||
}
|
||||
|
||||
code_block(
|
||||
"// Build atlas con 6 codepoints Tabler\n"
|
||||
"const uint16_t cps[] = {0xEB4D, 0xEAE5, 0xEAB9, 0xEB09, 0xEA4F, 0xEA88};\n"
|
||||
"IconAtlas* atlas = graph_icons_build(cps, 6, 32);\n"
|
||||
"\n"
|
||||
"// EntityTypes: cada uno con su shape e icono\n"
|
||||
"EntityType person = {0xFF6BCB77, SHAPE_CIRCLE, /*icon_id=*/1, 14, \"Person\"};\n"
|
||||
"EntityType email = {0xFFFF6B6B, SHAPE_SQUARE, /*icon_id=*/2, 14, \"Email\"};\n"
|
||||
"// ... etc\n"
|
||||
"\n"
|
||||
"// RelationTypes: solid / dashed / dotted\n"
|
||||
"RelationType knows = relation_type(0xFFCCCCCC, EDGE_SOLID, 1.5f, \"knows\");\n"
|
||||
"RelationType uses = relation_type(0xFFFFB870, EDGE_DASHED, 1.5f, \"uses\");\n"
|
||||
"\n"
|
||||
"// Bind atlas al renderer\n"
|
||||
"graph_renderer_set_icon_atlas(renderer, graph_icons_texture(atlas),\n"
|
||||
" graph_icons_uv_table(atlas),\n"
|
||||
" graph_icons_count(atlas));\n"
|
||||
"\n"
|
||||
"// Aristas direccionales\n"
|
||||
"GraphEdge e = graph_edge(src, tgt, 1.0f, /*type_id=*/1);\n"
|
||||
"e.flags |= EF_DIRECTED;");
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,108 +0,0 @@
|
||||
// Demo del primitivo viz/mesh_viewer.
|
||||
// Genera un cubo procedural in-line, lo sube al GPU, y permite cargar un
|
||||
// .obj desde un path ingresado en un text input.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/mesh_viewer.h"
|
||||
#include "gfx/mesh_obj_load.h"
|
||||
#include "gfx/mesh_gpu.h"
|
||||
#include "core/orbit_camera.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kCubeObj =
|
||||
"v -1 -1 -1\nv 1 -1 -1\nv 1 1 -1\nv -1 1 -1\n"
|
||||
"v -1 -1 1\nv 1 -1 1\nv 1 1 1\nv -1 1 1\n"
|
||||
"f 4 3 2 1\n" // back (-Z) — winding for outward normal
|
||||
"f 5 6 7 8\n" // front (+Z)
|
||||
"f 1 2 6 5\n" // bottom (-Y)
|
||||
"f 8 7 3 4\n" // top (+Y)
|
||||
"f 5 8 4 1\n" // left (-X)
|
||||
"f 2 3 7 6\n"; // right (+X)
|
||||
|
||||
struct State {
|
||||
fn::gfx::MeshGpu mesh{};
|
||||
fn::core::OrbitCamera cam{};
|
||||
char path[512] = "";
|
||||
std::string status;
|
||||
bool wireframe = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
State& state() {
|
||||
static State s;
|
||||
return s;
|
||||
}
|
||||
|
||||
void load_cube() {
|
||||
auto& s = state();
|
||||
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
|
||||
auto cpu = fn::gfx::mesh_obj_parse(kCubeObj, std::strlen(kCubeObj));
|
||||
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
|
||||
s.status = s.mesh.ok()
|
||||
? ("loaded cube: " + std::to_string(s.mesh.index_count / 3) + " tris")
|
||||
: "cube upload failed";
|
||||
}
|
||||
|
||||
void load_from_path() {
|
||||
auto& s = state();
|
||||
if (!s.path[0]) { s.status = "path is empty"; return; }
|
||||
auto cpu = fn::gfx::mesh_obj_load(s.path);
|
||||
if (cpu.positions.empty()) { s.status = "parse/read failed"; return; }
|
||||
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
|
||||
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
|
||||
s.status = s.mesh.ok()
|
||||
? ("loaded: " + std::to_string(s.mesh.index_count / 3) + " tris")
|
||||
: "upload failed";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_mesh_viewer() {
|
||||
demo_header("mesh_viewer", "v1.0.0",
|
||||
"Visualizador 3D para inspeccion de geometria. Composicion de "
|
||||
"mesh_obj_load (parser .obj puro) + mesh_gpu (upload VAO/VBO/EBO) + "
|
||||
"orbit_camera (drag/wheel) + mesh_viewer (FBO + ImGui::Image + Lambert).");
|
||||
|
||||
auto& s = state();
|
||||
if (!s.initialized) {
|
||||
load_cube();
|
||||
s.initialized = true;
|
||||
}
|
||||
|
||||
// Controls row.
|
||||
if (ImGui::Button("Reload cube")) load_cube();
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Wireframe", &s.wireframe);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("|");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(360);
|
||||
ImGui::InputTextWithHint("##obj_path", "absolute path to .obj", s.path, sizeof(s.path));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load .obj")) load_from_path();
|
||||
|
||||
ImGui::TextDisabled("status: %s | tris: %d | drag to orbit, wheel to zoom",
|
||||
s.status.c_str(),
|
||||
s.mesh.ok() ? s.mesh.index_count / 3 : 0);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
fn::viz::MeshViewerConfig cfg{};
|
||||
cfg.mesh = &s.mesh;
|
||||
cfg.cam = &s.cam;
|
||||
cfg.size = ImVec2(-1.0f, 480.0f);
|
||||
cfg.color = IM_COL32(160, 200, 255, 255);
|
||||
cfg.wireframe = s.wireframe;
|
||||
fn::viz::mesh_viewer("##gallery_mesh_viewer", cfg);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,208 +0,0 @@
|
||||
// demos_scientific.cpp — demos para los 5 charts cientificos del issue 0034:
|
||||
// treemap, sankey, chord, contour, voronoi.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/treemap.h"
|
||||
#include "viz/sankey.h"
|
||||
#include "viz/chord.h"
|
||||
#include "viz/contour.h"
|
||||
#include "viz/voronoi.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// treemap
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_treemap() {
|
||||
demo_header("treemap", "v1.0.0",
|
||||
"Squarified treemap (Bruls et al.) para jerarquias planas con valores. "
|
||||
"Algoritmo puro separado del render.");
|
||||
|
||||
section("Gastos por categoria");
|
||||
{
|
||||
std::vector<TreemapItem> items = {
|
||||
{"vivienda", 950.0f, IM_COL32(180, 120, 200, 255)},
|
||||
{"comida", 320.0f, IM_COL32(120, 180, 200, 255)},
|
||||
{"transporte", 180.0f, IM_COL32(200, 180, 120, 255)},
|
||||
{"ocio", 140.0f, IM_COL32(200, 120, 160, 255)},
|
||||
{"salud", 90.0f, IM_COL32(120, 200, 160, 255)},
|
||||
{"otros", 60.0f, IM_COL32(160, 160, 200, 255)},
|
||||
};
|
||||
treemap("##gastos", items, ImVec2(-1, 320));
|
||||
}
|
||||
|
||||
code_block(
|
||||
"std::vector<TreemapItem> items = {\n"
|
||||
" {\"vivienda\", 950.0f, IM_COL32(180,120,200,255)},\n"
|
||||
" {\"comida\", 320.0f, IM_COL32(120,180,200,255)},\n"
|
||||
" ...\n"
|
||||
"};\n"
|
||||
"treemap(\"##gastos\", items, ImVec2(-1, 320));"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// sankey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_sankey() {
|
||||
demo_header("sankey", "v1.0.0",
|
||||
"Sankey diagram para flujos source -> target. BFS topologico para columnas, "
|
||||
"bandas curvas (bezier cubico) para los links. Asume DAG.");
|
||||
|
||||
section("Clientes -> productos -> categorias");
|
||||
{
|
||||
std::vector<SankeyNode> nodes = {
|
||||
{"premium"}, {"basicos"},
|
||||
{"laptops"}, {"phones"}, {"tablets"},
|
||||
{"hardware"}, {"software"}, {"servicios"},
|
||||
};
|
||||
std::vector<SankeyLink> links = {
|
||||
// clientes -> productos
|
||||
{0, 2, 80}, {0, 3, 30}, {0, 4, 15},
|
||||
{1, 3, 60}, {1, 4, 40}, {1, 2, 20},
|
||||
// productos -> categorias
|
||||
{2, 5, 70}, {2, 6, 30},
|
||||
{3, 5, 50}, {3, 7, 40},
|
||||
{4, 6, 35}, {4, 7, 20},
|
||||
};
|
||||
sankey("##flow", nodes, links, ImVec2(-1, 360));
|
||||
}
|
||||
|
||||
code_block(
|
||||
"std::vector<SankeyNode> nodes = {{\"premium\"}, {\"basicos\"}, ...};\n"
|
||||
"std::vector<SankeyLink> links = {{0, 2, 80}, {0, 3, 30}, ...};\n"
|
||||
"sankey(\"##flow\", nodes, links, ImVec2(-1, 360));"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// chord
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_chord() {
|
||||
demo_header("chord", "v1.0.0",
|
||||
"Chord diagram para matrices NxN. Arcos proporcionales a sum(row) + cuerdas "
|
||||
"internas con bezier cubico.");
|
||||
|
||||
section("Flujos entre paises (matriz 6x6 simetrica)");
|
||||
{
|
||||
constexpr int N = 6;
|
||||
// simetrica de "comercio" entre 6 paises
|
||||
static float M[N * N] = {
|
||||
0, 10, 6, 12, 4, 3,
|
||||
10, 0, 14, 3, 8, 2,
|
||||
6, 14, 0, 9, 11, 5,
|
||||
12, 3, 9, 0, 7, 6,
|
||||
4, 8, 11, 7, 0, 13,
|
||||
3, 2, 5, 6, 13, 0,
|
||||
};
|
||||
static const char* labels[N] = {"ESP", "FRA", "ITA", "DEU", "PRT", "GBR"};
|
||||
chord("##chord", M, N, labels, ImVec2(420, 420));
|
||||
}
|
||||
|
||||
code_block(
|
||||
"float M[N*N] = { // simetrica\n"
|
||||
" 0, 10, 6, 12, 4, 3,\n"
|
||||
" 10, 0, 14, 3, 8, 2,\n"
|
||||
" ...\n"
|
||||
"};\n"
|
||||
"const char* labels[6] = {\"ESP\",\"FRA\",\"ITA\",\"DEU\",\"PRT\",\"GBR\"};\n"
|
||||
"chord(\"##c\", M, 6, labels);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// contour
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_contour() {
|
||||
demo_header("contour", "v1.0.0",
|
||||
"Contour plot 2D via marching squares. Para una gaussiana centrada los "
|
||||
"contornos resultantes son aproximadamente concentricos.");
|
||||
|
||||
constexpr int N = 32;
|
||||
static float grid[N * N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
// Mezcla de 2 gaussianas (peak central + secundario)
|
||||
for (int y = 0; y < N; y++) {
|
||||
for (int x = 0; x < N; x++) {
|
||||
float dx1 = x - N * 0.45f, dy1 = y - N * 0.5f;
|
||||
float dx2 = x - N * 0.75f, dy2 = y - N * 0.3f;
|
||||
float v = std::exp(-(dx1 * dx1 + dy1 * dy1) / 70.0f)
|
||||
+ 0.55f * std::exp(-(dx2 * dx2 + dy2 * dy2) / 30.0f);
|
||||
grid[y * N + x] = v;
|
||||
}
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
static const float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};
|
||||
contour("##gauss", grid, N, N, levels, 5, ImVec2(-1, 320));
|
||||
|
||||
code_block(
|
||||
"constexpr int N = 32;\n"
|
||||
"float grid[N*N];\n"
|
||||
"for (int y = 0; y < N; y++)\n"
|
||||
" for (int x = 0; x < N; x++) {\n"
|
||||
" float dx = x - N/2.0f, dy = y - N/2.0f;\n"
|
||||
" grid[y*N + x] = std::exp(-(dx*dx + dy*dy) / 80.0f);\n"
|
||||
" }\n"
|
||||
"float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};\n"
|
||||
"contour(\"##gauss\", grid, N, N, levels, 5);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// voronoi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_voronoi() {
|
||||
demo_header("voronoi", "v1.0.0",
|
||||
"Diagrama de Voronoi via raster brute-force (MVP). Tiles 4x4 px coloreados "
|
||||
"por el seed mas cercano. Suficiente para N <= 200.");
|
||||
|
||||
constexpr int N = 30;
|
||||
static ImVec2 seeds [N];
|
||||
static ImU32 colors[N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
unsigned seed = 7;
|
||||
auto rnd = [&]() {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
return (float)((seed >> 16) & 0x7fff) / 32768.0f;
|
||||
};
|
||||
// El render escala automaticamente; las posiciones se asumen en coords del rect.
|
||||
// Como no sabemos W/H aqui, usamos coords aproximadas para 600x300 y el clip
|
||||
// dentro de voronoi se encarga de mantenerlas en rango.
|
||||
for (int i = 0; i < N; i++) {
|
||||
seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);
|
||||
colors[i] = IM_COL32(40 + (int)(rnd() * 200),
|
||||
40 + (int)(rnd() * 200),
|
||||
60 + (int)(rnd() * 195),
|
||||
230);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
voronoi("##v", seeds, N, colors, ImVec2(-1, 300));
|
||||
|
||||
code_block(
|
||||
"ImVec2 seeds[30];\n"
|
||||
"ImU32 colors[30];\n"
|
||||
"for (int i = 0; i < 30; i++) {\n"
|
||||
" seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);\n"
|
||||
" colors[i] = IM_COL32(rnd_byte(), rnd_byte(), rnd_byte(), 230);\n"
|
||||
"}\n"
|
||||
"voronoi(\"##v\", seeds, 30, colors, ImVec2(-1, 300));"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,129 +0,0 @@
|
||||
// Demo de sql_workbench (Core, issue 0032).
|
||||
//
|
||||
// Abre `registry.db` en modo readonly y deja que el componente liste sus
|
||||
// tablas en la sidebar. La idea es probar el ciclo Run + tabla + historial
|
||||
// contra una DB real sin riesgo de mutarla.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "core/sql_workbench.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
namespace {
|
||||
|
||||
struct SqlDemoState {
|
||||
sqlite3* db = nullptr;
|
||||
fn::SqlWorkbenchState wb;
|
||||
bool tried_open = false;
|
||||
std::string db_path;
|
||||
std::string open_error;
|
||||
};
|
||||
|
||||
SqlDemoState& state() {
|
||||
static SqlDemoState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Resuelve la ruta a registry.db: env FN_REGISTRY_ROOT/registry.db si existe,
|
||||
// si no, prueba ./registry.db, ../registry.db, ../../registry.db (build tree).
|
||||
std::string resolve_registry_db() {
|
||||
if (const char* env = std::getenv("FN_REGISTRY_ROOT")) {
|
||||
std::string p = std::string(env) + "/registry.db";
|
||||
if (FILE* f = std::fopen(p.c_str(), "rb")) { std::fclose(f); return p; }
|
||||
}
|
||||
const char* candidates[] = {
|
||||
"registry.db",
|
||||
"../registry.db",
|
||||
"../../registry.db",
|
||||
"../../../registry.db",
|
||||
"../../../../registry.db",
|
||||
};
|
||||
for (const char* c : candidates) {
|
||||
if (FILE* f = std::fopen(c, "rb")) { std::fclose(f); return c; }
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void ensure_open() {
|
||||
auto& s = state();
|
||||
if (s.tried_open) return;
|
||||
s.tried_open = true;
|
||||
|
||||
s.db_path = resolve_registry_db();
|
||||
if (s.db_path.empty()) {
|
||||
s.open_error = "registry.db not found (tried FN_REGISTRY_ROOT and parent dirs)";
|
||||
return;
|
||||
}
|
||||
int rc = sqlite3_open_v2(s.db_path.c_str(), &s.db,
|
||||
SQLITE_OPEN_READONLY, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
s.open_error = sqlite3_errmsg(s.db);
|
||||
if (s.db) { sqlite3_close(s.db); s.db = nullptr; }
|
||||
return;
|
||||
}
|
||||
s.wb.readonly = true;
|
||||
// Query inicial mas util para el demo: lista de funciones del registry.
|
||||
s.wb.query =
|
||||
"SELECT id, kind, purity, domain\n"
|
||||
"FROM functions\n"
|
||||
"ORDER BY id\n"
|
||||
"LIMIT 50;";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_sql_workbench() {
|
||||
using namespace fn_tokens;
|
||||
|
||||
demo_header("sql_workbench", "v1.0.0",
|
||||
"Workbench SQL: editor con highlighting, schema sidebar, tabla de "
|
||||
"resultados e historial. Ejecuta queries contra una sqlite3* del caller. "
|
||||
"En este demo, registry.db abierto en modo readonly.");
|
||||
|
||||
ensure_open();
|
||||
auto& s = state();
|
||||
|
||||
if (!s.open_error.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
|
||||
ImGui::TextWrapped("could not open registry.db: %s", s.open_error.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::TextWrapped("Set FN_REGISTRY_ROOT to the repo root or run from the repo cwd.");
|
||||
ImGui::PopStyleColor();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::Text("db: %s (readonly)", s.db_path.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
section("workbench");
|
||||
{
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
// Reserva un pelin para el code_block de abajo.
|
||||
float h = avail.y - 110.0f;
|
||||
if (h < 320.0f) h = 320.0f;
|
||||
fn::sql_workbench("##gallery_sql", s.db, s.wb, ImVec2(-1, h));
|
||||
}
|
||||
|
||||
code_block(
|
||||
"sqlite3* db = nullptr;\n"
|
||||
"sqlite3_open_v2(\"registry.db\", &db, SQLITE_OPEN_READONLY, nullptr);\n"
|
||||
"fn::SqlWorkbenchState st;\n"
|
||||
"st.readonly = true;\n"
|
||||
"fn::sql_workbench(\"##sql\", db, st, ImVec2(-1, -1));"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,279 +0,0 @@
|
||||
// Demos individuales de text_editor y file_watcher (Wave 1, issue 0025).
|
||||
//
|
||||
// Aunque las dos primitivas estan diseñadas para componerse, en gallery se
|
||||
// muestran por separado para que cada entry exhiba un solo primitivo y su
|
||||
// API minima.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "core/text_editor.h"
|
||||
#include "core/file_watcher.h"
|
||||
#include "core/button.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ===========================================================================
|
||||
// text_editor — editor de codigo con syntax highlighting
|
||||
// ===========================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kSampleGLSL =
|
||||
"#version 330\n"
|
||||
"// fragment shader demo\n"
|
||||
"out vec4 frag_color;\n"
|
||||
"uniform vec2 u_resolution;\n"
|
||||
"uniform float u_time;\n"
|
||||
"\n"
|
||||
"void main() {\n"
|
||||
" vec2 uv = gl_FragCoord.xy / u_resolution;\n"
|
||||
" vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0,2,4));\n"
|
||||
" frag_color = vec4(col, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
const char* kSampleSQL =
|
||||
"-- fts5 search sobre el registry\n"
|
||||
"SELECT id, kind, purity, description\n"
|
||||
"FROM functions\n"
|
||||
"WHERE id IN (\n"
|
||||
" SELECT id FROM functions_fts\n"
|
||||
" WHERE functions_fts MATCH 'name:slic* OR description:slic*'\n"
|
||||
")\n"
|
||||
"ORDER BY name\n"
|
||||
"LIMIT 50;\n";
|
||||
|
||||
const char* kSampleCpp =
|
||||
"#include <imgui.h>\n"
|
||||
"namespace fn {\n"
|
||||
" bool button(const char* label, ButtonVariant v) {\n"
|
||||
" auto& tk = tokens::current();\n"
|
||||
" ImGui::PushStyleColor(ImGuiCol_Button, tk.bg_for(v));\n"
|
||||
" bool clicked = ImGui::Button(label);\n"
|
||||
" ImGui::PopStyleColor();\n"
|
||||
" return clicked;\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
|
||||
struct EditorState {
|
||||
fn::TextEditorState* ed = nullptr;
|
||||
fn::CodeLang lang = fn::CodeLang::GLSL;
|
||||
};
|
||||
|
||||
EditorState& editor_state() {
|
||||
static EditorState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
void ensure_editor() {
|
||||
auto& s = editor_state();
|
||||
if (!s.ed) {
|
||||
s.ed = fn::text_editor_create(s.lang);
|
||||
fn::text_editor_set_text(s.ed, kSampleGLSL);
|
||||
}
|
||||
}
|
||||
|
||||
void apply_language(fn::CodeLang next) {
|
||||
auto& s = editor_state();
|
||||
if (next == s.lang) return;
|
||||
fn::text_editor_destroy(s.ed);
|
||||
s.ed = fn::text_editor_create(next);
|
||||
s.lang = next;
|
||||
switch (next) {
|
||||
case fn::CodeLang::GLSL: fn::text_editor_set_text(s.ed, kSampleGLSL); break;
|
||||
case fn::CodeLang::SQL: fn::text_editor_set_text(s.ed, kSampleSQL); break;
|
||||
case fn::CodeLang::Cpp: fn::text_editor_set_text(s.ed, kSampleCpp); break;
|
||||
case fn::CodeLang::Generic: fn::text_editor_set_text(s.ed, ""); break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_text_editor() {
|
||||
using namespace fn_tokens;
|
||||
|
||||
demo_header("text_editor", "v1.0.0",
|
||||
"Editor de codigo embebido en ImGui con syntax highlighting (GLSL/SQL/Cpp/Generic). "
|
||||
"Wrapper PIMPL sobre ImGuiColorTextEdit (MIT). API: create / set_text / get_text / "
|
||||
"render / is_dirty.");
|
||||
|
||||
ensure_editor();
|
||||
auto& s = editor_state();
|
||||
|
||||
section("language");
|
||||
{
|
||||
const char* labels[] = {"GLSL", "SQL", "Cpp", "Generic"};
|
||||
const fn::CodeLang langs[] = {
|
||||
fn::CodeLang::GLSL, fn::CodeLang::SQL, fn::CodeLang::Cpp, fn::CodeLang::Generic
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (i > 0) ImGui::SameLine();
|
||||
bool active = (s.lang == langs[i]);
|
||||
if (active) ImGui::PushStyleColor(ImGuiCol_Button, colors::primary);
|
||||
if (ImGui::Button(labels[i])) apply_language(langs[i]);
|
||||
if (active) ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
section("editor");
|
||||
{
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
float h = avail.y - 60.0f;
|
||||
if (h < 220.0f) h = 220.0f;
|
||||
fn::text_editor_render(s.ed, "##fn_text_editor_solo", ImVec2(-1, h));
|
||||
|
||||
if (fn::text_editor_is_dirty(s.ed)) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::warning);
|
||||
ImGui::TextUnformatted("(modified)");
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("clear dirty##te_solo")) fn::text_editor_clear_dirty(s.ed);
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextUnformatted("(clean)");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
code_block(
|
||||
"auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);\n"
|
||||
"fn::text_editor_set_text(ed, src);\n"
|
||||
"if (fn::text_editor_render(ed, \"##ed\", {600, 400}))\n"
|
||||
" on_changed(fn::text_editor_get_text(ed));"
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// file_watcher — watcher cross-platform no bloqueante
|
||||
// ===========================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kWatchPath = "/tmp/fn_demo.glsl";
|
||||
|
||||
struct WatcherDemoState {
|
||||
fn::FileWatcher* fw = nullptr;
|
||||
bool active = false;
|
||||
std::string err;
|
||||
std::deque<std::string> events;
|
||||
};
|
||||
|
||||
WatcherDemoState& watcher_state() {
|
||||
static WatcherDemoState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
void ensure_watcher() {
|
||||
auto& s = watcher_state();
|
||||
if (!s.fw) {
|
||||
s.fw = fn::file_watcher_create();
|
||||
s.active = fn::file_watcher_add(s.fw, kWatchPath);
|
||||
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
|
||||
}
|
||||
}
|
||||
|
||||
const char* kind_label(fn::FileEvent::Kind k) {
|
||||
switch (k) {
|
||||
case fn::FileEvent::Modified: return "MODIFIED";
|
||||
case fn::FileEvent::Created: return "CREATED";
|
||||
case fn::FileEvent::Deleted: return "DELETED";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
void poll_and_log() {
|
||||
auto& s = watcher_state();
|
||||
if (!s.fw) return;
|
||||
auto evs = fn::file_watcher_poll(s.fw);
|
||||
for (auto& e : evs) {
|
||||
char buf[512];
|
||||
std::snprintf(buf, sizeof(buf), "[%s] %s", kind_label(e.kind), e.path.c_str());
|
||||
s.events.push_back(buf);
|
||||
}
|
||||
while (s.events.size() > 200) s.events.pop_front();
|
||||
}
|
||||
|
||||
bool touch_demo_file(std::string& err_out) {
|
||||
FILE* f = std::fopen(kWatchPath, "a");
|
||||
if (!f) { err_out = std::strerror(errno); return false; }
|
||||
std::fprintf(f, "// touch %ld\n", (long)std::time(nullptr));
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_file_watcher() {
|
||||
using namespace fn_tokens;
|
||||
|
||||
demo_header("file_watcher", "v1.0.0",
|
||||
"Watcher de archivos cross-platform no bloqueante. Linux: inotify. Windows: "
|
||||
"ReadDirectoryChangesW. API: create / add / poll (drain) / destroy. Cap del "
|
||||
"buffer de eventos: 200.");
|
||||
|
||||
ensure_watcher();
|
||||
poll_and_log();
|
||||
|
||||
auto& s = watcher_state();
|
||||
|
||||
section("watcher state");
|
||||
ImGui::Text("path: %s", kWatchPath);
|
||||
ImGui::Text("active: %s", s.active ? "yes" : "no");
|
||||
if (!s.err.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
|
||||
ImGui::TextWrapped("err: %s", s.err.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
section("trigger events");
|
||||
{
|
||||
if (ImGui::Button("touch (append timestamp)")) {
|
||||
std::string e;
|
||||
if (!touch_demo_file(e)) s.err = "touch failed: " + e;
|
||||
else s.err.clear();
|
||||
// Si el archivo no existia al inicio, reintenta el add.
|
||||
if (!s.active) {
|
||||
s.active = fn::file_watcher_add(s.fw, kWatchPath);
|
||||
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("clear events")) s.events.clear();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(o desde otro terminal: echo hi >> %s)", kWatchPath);
|
||||
}
|
||||
|
||||
section("event log");
|
||||
ImGui::Text("captured: %d", (int)s.events.size());
|
||||
ImGui::BeginChild("##fw_evlog", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||
if (s.events.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextWrapped("Sin eventos. Pulsa touch o modifica el path desde otro terminal.");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) {
|
||||
ImGui::TextUnformatted(it->c_str());
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
code_block(
|
||||
"auto* fw = fn::file_watcher_create();\n"
|
||||
"fn::file_watcher_add(fw, \"/tmp/foo.glsl\");\n"
|
||||
"for (auto& e : fn::file_watcher_poll(fw)) {\n"
|
||||
" handle_event(e.path, e.kind);\n"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,211 +0,0 @@
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/bar_chart.h"
|
||||
#include "viz/pie_chart.h"
|
||||
#include "viz/line_plot.h"
|
||||
#include "viz/scatter_plot.h"
|
||||
#include "viz/histogram.h"
|
||||
#include "viz/sparkline.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bar_chart
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_bar_chart() {
|
||||
demo_header("bar_chart", "v1.2.0",
|
||||
"Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados "
|
||||
"de labels cuando no caben horizontalmente.");
|
||||
|
||||
section("Labels que caben horizontalmente");
|
||||
{
|
||||
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
|
||||
float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f};
|
||||
bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f);
|
||||
}
|
||||
|
||||
section("Labels largos que obligan a rotar");
|
||||
{
|
||||
const char* domains[] = {
|
||||
"core", "infrastructure", "finance", "datascience",
|
||||
"cybersecurity", "notebook", "browser"
|
||||
};
|
||||
float values[] = {412, 187, 94, 63, 42, 38, 22};
|
||||
bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f);
|
||||
}
|
||||
|
||||
code_block(
|
||||
"const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
|
||||
"float values[] = {412,187,94,63,36};\n"
|
||||
"bar_chart(\"##lang\", labels, values, 5); // h=200 default\n"
|
||||
"bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// pie_chart
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_pie_chart() {
|
||||
demo_header("pie_chart", "v1.1.0",
|
||||
"Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con "
|
||||
"valor absoluto + porcentaje.");
|
||||
|
||||
if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) {
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
{
|
||||
const char* labels[] = {"Pure", "Impure"};
|
||||
float values[] = {412.0f, 278.0f};
|
||||
variant_label("Pie (radius auto)");
|
||||
pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f);
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
{
|
||||
const char* labels[] = {"function", "pipeline", "component"};
|
||||
float values[] = {618.0f, 42.0f, 230.0f};
|
||||
variant_label("Donut (radius = -0.45)");
|
||||
pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
code_block(
|
||||
"const char* labels[] = {\"Pure\",\"Impure\"};\n"
|
||||
"float values[] = {412, 278};\n"
|
||||
"pie_chart(\"##p\", labels, values, 2); // pie auto\n"
|
||||
"pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// line_plot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_line_plot() {
|
||||
demo_header("line_plot", "v1.1.0",
|
||||
"Line plot 2D con limites de ejes calculados de min/max y pineados. "
|
||||
"Sin auto-fit animado, sin pan/zoom.");
|
||||
|
||||
constexpr int N = 100;
|
||||
static float xs[N], ys[N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
for (int i = 0; i < N; i++) {
|
||||
xs[i] = static_cast<float>(i) * 0.1f;
|
||||
ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
line_plot("##line", xs, ys, N, 240.0f);
|
||||
|
||||
code_block(
|
||||
"line_plot(\"##series\", xs, ys, count); // h=200 default\n"
|
||||
"line_plot(\"##series\", xs, ys, count, 300.0f); // custom height"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// scatter_plot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_scatter_plot() {
|
||||
demo_header("scatter_plot", "v1.1.0",
|
||||
"Puntos dispersos con ejes pineados (5% headroom). Sin interaccion.");
|
||||
|
||||
constexpr int N = 120;
|
||||
static float xs[N], ys[N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
unsigned seed = 1234;
|
||||
auto rnd = [&]() {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
|
||||
};
|
||||
for (int i = 0; i < N; i++) {
|
||||
xs[i] = rnd() * 10.0f;
|
||||
ys[i] = 0.5f * xs[i] + rnd() * 3.0f;
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
scatter_plot("##sc", xs, ys, N, 240.0f);
|
||||
|
||||
code_block(
|
||||
"scatter_plot(\"##xy\", xs, ys, count, 240.0f);"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// histogram
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_histogram() {
|
||||
demo_header("histogram", "v1.1.0",
|
||||
"Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit "
|
||||
"para los bins + Lock para bloquear pan/zoom.");
|
||||
|
||||
constexpr int N = 300;
|
||||
static float vals[N];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
unsigned seed = 42;
|
||||
auto rnd = [&]() {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
|
||||
};
|
||||
// Aproximacion de distribucion normal via box-muller simplificado
|
||||
for (int i = 0; i < N; i++) {
|
||||
float u1 = rnd() + 1e-6f;
|
||||
float u2 = rnd();
|
||||
vals[i] = std::sqrt(-2.0f * std::log(u1))
|
||||
* std::cos(2.0f * 3.14159f * u2);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
histogram("##hist", vals, N, -1, 240.0f);
|
||||
|
||||
code_block(
|
||||
"histogram(\"##h\", values, count); // bins=Sturges\n"
|
||||
"histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// sparkline
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void demo_sparkline() {
|
||||
demo_header("sparkline", "v1.0.0",
|
||||
"Mini grafico de lineas inline (rellenado con alpha + linea). "
|
||||
"Pensado para tablas, KPI cards, headers.");
|
||||
|
||||
float up[] = {10, 12, 11, 15, 18, 17, 20};
|
||||
float down[] = {30, 28, 29, 25, 22, 24, 20};
|
||||
float flat[] = {10, 10, 10, 10, 10, 10, 10};
|
||||
|
||||
ImGui::Text("Trending up "); ImGui::SameLine();
|
||||
sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f);
|
||||
|
||||
ImGui::Text("Trending down"); ImGui::SameLine();
|
||||
sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f);
|
||||
|
||||
ImGui::Text("Flat "); ImGui::SameLine();
|
||||
sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f);
|
||||
|
||||
code_block(
|
||||
"float history[] = {10,12,11,15,18,17,20};\n"
|
||||
"sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);"
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -1,230 +0,0 @@
|
||||
// primitives_gallery — catalogo visual interactivo de los primitivos UI
|
||||
// del registry (cpp/functions/core y cpp/functions/viz).
|
||||
//
|
||||
// Sidebar izquierdo con lista de primitivos agrupados por dominio; panel
|
||||
// derecho renderiza la demo del item seleccionado (+ snippet de codigo).
|
||||
//
|
||||
// Rol: smoke test visual + documentacion viva + build gate en CI.
|
||||
// NO se conecta a sqlite_api ni a ningun backend. Datos sinteticos.
|
||||
|
||||
#include "app_base.h"
|
||||
#include "imgui.h"
|
||||
#include "core/fullscreen_window.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/page_header.h"
|
||||
#include "core/toast.h"
|
||||
#include "core/app_menubar.h"
|
||||
#include "core/tree_view.h"
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
#include "capture.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
|
||||
struct DemoEntry {
|
||||
const char* id; // id estable, apto para comparar seleccion
|
||||
const char* label; // texto en sidebar
|
||||
const char* category; // "Core" o "Viz"
|
||||
void (*fn)(); // puntero a la demo_xxx
|
||||
};
|
||||
|
||||
static const DemoEntry k_demos[] = {
|
||||
// Core
|
||||
{"button", "button", "Core", &gallery::demo_button},
|
||||
{"icon_button", "icon_button", "Core", &gallery::demo_icon_button},
|
||||
{"toolbar", "toolbar", "Core", &gallery::demo_toolbar},
|
||||
{"modal_dialog", "modal_dialog", "Core", &gallery::demo_modal},
|
||||
{"text_input", "text_input", "Core", &gallery::demo_text_input},
|
||||
{"select", "select", "Core", &gallery::demo_select},
|
||||
{"toast", "toast + inbox", "Core", &gallery::demo_toast},
|
||||
{"tree_view", "tree_view", "Core", &gallery::demo_tree_view},
|
||||
{"badge", "badge", "Core", &gallery::demo_badge},
|
||||
{"empty_state", "empty_state", "Core", &gallery::demo_empty_state},
|
||||
{"page_header", "page_header", "Core", &gallery::demo_page_header},
|
||||
{"dashboard_panel", "dashboard_panel", "Core", &gallery::demo_dashboard_panel},
|
||||
{"kpi_card", "kpi_card", "Core", &gallery::demo_kpi_card},
|
||||
{"text_editor", "text_editor", "Core", &gallery::demo_text_editor}, // wave 1
|
||||
{"file_watcher", "file_watcher", "Core", &gallery::demo_file_watcher}, // wave 1
|
||||
{"process_runner", "process_runner", "Core", &gallery::demo_process_runner},
|
||||
{"tween", "tween_curves", "Core", &gallery::demo_tween},
|
||||
{"bezier_editor", "bezier_editor", "Core", &gallery::demo_bezier_editor},
|
||||
{"timeline", "timeline", "Core", &gallery::demo_timeline},
|
||||
{"sql_workbench", "sql_workbench", "Core", &gallery::demo_sql_workbench}, // issue 0032
|
||||
// Viz
|
||||
{"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart},
|
||||
{"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart},
|
||||
{"line_plot", "line_plot", "Viz", &gallery::demo_line_plot},
|
||||
{"scatter_plot", "scatter_plot", "Viz", &gallery::demo_scatter_plot},
|
||||
{"histogram", "histogram", "Viz", &gallery::demo_histogram},
|
||||
{"sparkline", "sparkline", "Viz", &gallery::demo_sparkline},
|
||||
{"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph},
|
||||
{"graph_styles", "graph_styles", "Viz", &gallery::demo_graph_styles}, // issue 0049f
|
||||
{"candlestick", "candlestick", "Viz", &gallery::demo_candlestick},
|
||||
{"gauge", "gauge", "Viz", &gallery::demo_gauge},
|
||||
{"heatmap", "heatmap", "Viz", &gallery::demo_heatmap},
|
||||
{"table_view", "table_view", "Viz", &gallery::demo_table_view},
|
||||
{"surface_plot_3d", "surface_plot_3d", "Viz", &gallery::demo_surface_plot_3d},
|
||||
{"scatter_3d", "scatter_3d", "Viz", &gallery::demo_scatter_3d},
|
||||
{"mesh_viewer", "mesh_viewer", "Viz", &gallery::demo_mesh_viewer},
|
||||
{"treemap", "treemap", "Viz", &gallery::demo_treemap},
|
||||
{"sankey", "sankey", "Viz", &gallery::demo_sankey},
|
||||
{"chord", "chord", "Viz", &gallery::demo_chord},
|
||||
{"contour", "contour", "Viz", &gallery::demo_contour},
|
||||
{"voronoi", "voronoi", "Viz", &gallery::demo_voronoi},
|
||||
// Gfx (shaders_lab core)
|
||||
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
|
||||
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
|
||||
{"gl_info", "gl_info", "Gfx", &gallery::demo_gl_info}, // issue 0049b
|
||||
};
|
||||
static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]);
|
||||
|
||||
static std::string g_selected_id = "button";
|
||||
|
||||
static const DemoEntry* find_demo(const std::string& id) {
|
||||
for (int i = 0; i < k_demo_count; i++) {
|
||||
if (id == k_demos[i].id) return &k_demos[i];
|
||||
}
|
||||
return &k_demos[0];
|
||||
}
|
||||
|
||||
static void draw_sidebar() {
|
||||
ImGui::BeginChild("##gallery_sidebar", ImVec2(220, 0),
|
||||
ImGuiChildFlags_Borders);
|
||||
|
||||
// Agrupar por categoria como rama del tree_view (categorias abiertas por
|
||||
// defecto). Cada demo es una hoja seleccionable.
|
||||
int i = 0;
|
||||
while (i < k_demo_count) {
|
||||
const char* category = k_demos[i].category;
|
||||
|
||||
// Default-open la rama la primera vez que se abre el sidebar.
|
||||
ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
|
||||
if (fn_ui::tree_branch_begin(category, category, /*selected=*/false)) {
|
||||
// Recorrer todas las demos consecutivas con esta misma categoria.
|
||||
while (i < k_demo_count
|
||||
&& std::strcmp(k_demos[i].category, category) == 0) {
|
||||
const auto& d = k_demos[i];
|
||||
const bool selected = (g_selected_id == d.id);
|
||||
fn_ui::tree_leaf(d.id, d.label, selected);
|
||||
if (fn_ui::tree_node_clicked()) {
|
||||
g_selected_id = d.id;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
fn_ui::tree_branch_end();
|
||||
} else {
|
||||
// Rama colapsada — saltar todos sus items.
|
||||
while (i < k_demo_count
|
||||
&& std::strcmp(k_demos[i].category, category) == 0) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
static void render() {
|
||||
// Theme y gl_loader gestionados por fn::run_app (theme=FnDark por defecto,
|
||||
// init_gl_loader=true en AppConfig). Menubar via run_app.
|
||||
// auto_dockspace=false porque usamos fullscreen_window que ocupa todo.
|
||||
|
||||
fullscreen_window_begin("##gallery");
|
||||
|
||||
page_header_begin("Primitives Gallery",
|
||||
"Visual catalog of fn_registry C++ UI primitives");
|
||||
page_header_end();
|
||||
|
||||
if (ImGui::BeginTable("##layout", 2,
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit)) {
|
||||
ImGui::TableSetupColumn("sidebar", ImGuiTableColumnFlags_WidthFixed, 220.0f);
|
||||
ImGui::TableSetupColumn("content", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
draw_sidebar();
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::BeginChild("##gallery_content", ImVec2(0, 0),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_HorizontalScrollbar);
|
||||
// Cuando cambia el tamaño de fuente (Settings > Size), el contenido
|
||||
// del child crece/encoge pero la posicion de scroll en pixeles
|
||||
// no — efecto: lo visible "se baja". Escalamos scroll_y por el
|
||||
// ratio de fuentes para mantener la misma linea logica arriba.
|
||||
{
|
||||
static float s_prev_font_size = 0.0f;
|
||||
float cur_font_size = ImGui::GetStyle().FontSizeBase;
|
||||
if (s_prev_font_size > 0.0f &&
|
||||
std::fabs(s_prev_font_size - cur_font_size) > 0.01f) {
|
||||
ImGui::SetScrollY(ImGui::GetScrollY() *
|
||||
(cur_font_size / s_prev_font_size));
|
||||
}
|
||||
s_prev_font_size = cur_font_size;
|
||||
}
|
||||
const DemoEntry* d = find_demo(g_selected_id);
|
||||
if (d && d->fn) d->fn();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
fullscreen_window_end();
|
||||
|
||||
// Toasts se renderizan encima para que el demo de toast funcione aqui tambien.
|
||||
fn_ui::toast_render();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Capture mode: `primitives_gallery --capture <output_dir>` corre cada
|
||||
// demo en una ventana GLFW invisible y guarda PNG por demo. Para CI/golden.
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (std::strcmp(argv[i], "--capture") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
std::fprintf(stderr, "--capture requires an output dir argument\n");
|
||||
return 2;
|
||||
}
|
||||
const char* out_dir = argv[i + 1];
|
||||
// Best-effort mkdir (idempotente). Windows mkdir() solo acepta el path.
|
||||
#if defined(_WIN32)
|
||||
mkdir(out_dir);
|
||||
#else
|
||||
mkdir(out_dir, 0755);
|
||||
#endif
|
||||
|
||||
std::vector<gallery::CaptureItem> items;
|
||||
items.reserve(k_demo_count);
|
||||
for (int j = 0; j < k_demo_count; j++) {
|
||||
items.push_back({k_demos[j].id, k_demos[j].fn});
|
||||
}
|
||||
|
||||
gallery::CaptureConfig cfg;
|
||||
cfg.output_dir = out_dir;
|
||||
cfg.warmup_frames = 3;
|
||||
cfg.capture_w = 800;
|
||||
cfg.capture_h = 600;
|
||||
const bool ok = gallery::run_capture(cfg, items);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return fn::run_app(
|
||||
{.title = "fn_registry · Primitives Gallery",
|
||||
.width = 1400,
|
||||
.height = 900,
|
||||
.viewports = true,
|
||||
.about = {.name = "Primitives Gallery",
|
||||
.version = "0.4.0",
|
||||
.description = "Visual catalog of fn_registry C++ UI primitives. Now on OpenGL 4.3 core (compute, SSBOs, image load/store) — ver demo gl_info."},
|
||||
.init_gl_loader = true,
|
||||
.auto_dockspace = false,
|
||||
.log = {"primitives_gallery.log", 1}},
|
||||
render
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
# Tables playground (cpp_apps.md / playgrounds.md). NO se indexa.
|
||||
# Build flag FN_TQL_DUCKDB=ON activa el adapter tql_duckdb (issue 0080).
|
||||
option(FN_TQL_DUCKDB "Enable DuckDB SQL execution adapter for tables playground" OFF)
|
||||
|
||||
set(_TABLES_SRC
|
||||
main.cpp
|
||||
data_table.cpp
|
||||
data_table_logic.cpp
|
||||
llm_anthropic.cpp
|
||||
lua_engine.cpp
|
||||
tql.cpp
|
||||
tql_to_sql.cpp
|
||||
viz.cpp
|
||||
)
|
||||
set(_TABLES_TEST_SRC
|
||||
self_test.cpp
|
||||
data_table_logic.cpp
|
||||
llm_anthropic.cpp
|
||||
lua_engine.cpp
|
||||
tql.cpp
|
||||
tql_to_sql.cpp
|
||||
)
|
||||
if(FN_TQL_DUCKDB)
|
||||
list(APPEND _TABLES_SRC tql_duckdb.cpp)
|
||||
list(APPEND _TABLES_TEST_SRC tql_duckdb.cpp)
|
||||
endif()
|
||||
|
||||
add_imgui_app(tables_playground ${_TABLES_SRC})
|
||||
target_link_libraries(tables_playground PRIVATE lua54 implot)
|
||||
if(FN_TQL_DUCKDB)
|
||||
target_compile_definitions(tables_playground PRIVATE FN_TQL_DUCKDB=1)
|
||||
target_link_libraries(tables_playground PRIVATE duckdb_vendored)
|
||||
duckdb_copy_runtime(tables_playground)
|
||||
endif()
|
||||
|
||||
# Self-test E2E (logica pura + lua_engine + tql).
|
||||
add_executable(tables_playground_self_test ${_TABLES_TEST_SRC})
|
||||
target_include_directories(tables_playground_self_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/functions
|
||||
)
|
||||
target_link_libraries(tables_playground_self_test PRIVATE lua54)
|
||||
if(FN_TQL_DUCKDB)
|
||||
target_compile_definitions(tables_playground_self_test PRIVATE FN_TQL_DUCKDB=1)
|
||||
target_link_libraries(tables_playground_self_test PRIVATE duckdb_vendored)
|
||||
duckdb_copy_runtime(tables_playground_self_test)
|
||||
endif()
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user