Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18bdfc7bfd | |||
| 27ae829a1e |
+1
-4
@@ -21,14 +21,12 @@ 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). `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`.
|
||||
**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.
|
||||
|
||||
**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`.
|
||||
|
||||
---
|
||||
@@ -260,7 +258,6 @@ 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,16 +30,6 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../projects/aurgi/.claude/commands
|
||||
@@ -1,36 +1,121 @@
|
||||
---
|
||||
description: "DEPRECADO 2026-05-19 — usa /autopilot. Wrapper directo a fn-orquestador conservado solo como debug primitive."
|
||||
# /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.
|
||||
|
||||
---
|
||||
|
||||
# /autonomous-task — DEPRECADO (sustituido por `/autopilot`)
|
||||
## Argumento
|
||||
|
||||
**ESTADO:** deprecado 2026-05-19. Usa `/autopilot <NNNN>` en su lugar.
|
||||
`$ARGUMENTS` — `<issue_id>` o `<task_spec_path>` + flags opcionales.
|
||||
|
||||
## Por que deprecado
|
||||
```
|
||||
/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
|
||||
```
|
||||
|
||||
`/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.
|
||||
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
|
||||
|
||||
Behaviour orquestador-side es identico. La unica diferencia es que `/autopilot` valida antes de delegar; `/autonomous-task` delegaba ciego.
|
||||
---
|
||||
|
||||
## Sustitucion 1:1
|
||||
## Comportamiento
|
||||
|
||||
| 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` |
|
||||
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`
|
||||
|
||||
## Modo debug
|
||||
---
|
||||
|
||||
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.
|
||||
## Reglas duras (no negociables)
|
||||
|
||||
## Migration deadline
|
||||
- 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/`.
|
||||
|
||||
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`.
|
||||
---
|
||||
|
||||
Ver `.claude/commands/autopilot.md` para spec completa.
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
---
|
||||
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`.
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
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`).
|
||||
@@ -1,274 +0,0 @@
|
||||
# /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
|
||||
@@ -1,186 +0,0 @@
|
||||
---
|
||||
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.
|
||||
```
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
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,20 +152,6 @@ 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`
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
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).
|
||||
@@ -1,170 +0,0 @@
|
||||
---
|
||||
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. |
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
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,7 +21,6 @@ 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) |
|
||||
@@ -35,6 +34,3 @@ 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. |
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
## 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.
|
||||
@@ -1,102 +0,0 @@
|
||||
## 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.
|
||||
+12
-233
@@ -20,14 +20,14 @@ Razones:
|
||||
|
||||
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
|
||||
|
||||
### 1. Ubicacion (issue 0096 estandarizada)
|
||||
### 1. Ubicacion
|
||||
|
||||
| Caso | Donde vive |
|
||||
|---|---|
|
||||
| App independiente | `apps/<nombre>/` |
|
||||
| App independiente | `cpp/apps/<nombre>/` |
|
||||
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
|
||||
|
||||
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`.
|
||||
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
|
||||
|
||||
### 2. Estructura minima
|
||||
|
||||
@@ -84,7 +84,6 @@ 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++
|
||||
@@ -103,7 +102,6 @@ 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`
|
||||
|
||||
@@ -191,105 +189,20 @@ 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 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.
|
||||
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).
|
||||
|
||||
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:
|
||||
Tests: `cpp/apps/altsnap_jitter_test/` corre dos 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` 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.
|
||||
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta
|
||||
que `render()` no se llama durante el bracket.
|
||||
|
||||
Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh &&
|
||||
e2e_run_cpp_windows altsnap_jitter_test`.
|
||||
Lanzar con `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
|
||||
@@ -348,137 +261,3 @@ 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,18 +20,10 @@ 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 |
|
||||
@@ -41,8 +33,7 @@ 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` (conformance) | `audit_cpp_apps_go_infra` |
|
||||
| `cpp-apps` (table migration) | `audit_cpp_table_migration_go_infra` (inline en `audit_cpp_apps.go`) |
|
||||
| `cpp-apps` | `audit_cpp_apps_go_infra` |
|
||||
|
||||
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.
|
||||
|
||||
@@ -73,8 +64,6 @@ 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,26 +28,3 @@ 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,35 +1,3 @@
|
||||
## ids_naming — formato predictible
|
||||
IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`).
|
||||
|
||||
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.
|
||||
```
|
||||
Nombres de funciones en snake_case. Tipos en PascalCase para Go.
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
## 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 / 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`.
|
||||
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
|
||||
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
|
||||
- Service de produccion recibiendo HTTP.
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,121 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,165 +0,0 @@
|
||||
#!/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,10 +81,3 @@ 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,68 +8,6 @@ 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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[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
|
||||
@@ -1,360 +0,0 @@
|
||||
# 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, hub *DagRunHub, frontendFS fs.FS) {
|
||||
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, frontendFS fs.FS) {
|
||||
// API routes.
|
||||
mux.HandleFunc("GET /api/dags", handleListDags(executor))
|
||||
mux.HandleFunc("GET /api/dags/{name}", handleGetDag(executor))
|
||||
@@ -15,18 +15,10 @@ func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, h
|
||||
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))
|
||||
|
||||
+7
-65
@@ -2,8 +2,7 @@
|
||||
name: dag_engine
|
||||
lang: go
|
||||
domain: infra
|
||||
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."
|
||||
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."
|
||||
tags: [service, dag, workflow, scheduler, web, cron]
|
||||
uses_functions:
|
||||
- dag_parse_go_core
|
||||
@@ -28,18 +27,6 @@ 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
|
||||
@@ -84,60 +71,15 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
|
||||
|
||||
```bash
|
||||
# CLI
|
||||
./dag-engine run apps/dag_engine/dags_migrated/fn_backup.yaml
|
||||
./dag-engine list apps/dag_engine/dags_migrated/
|
||||
./dag-engine run ~/dagu/dags/example.yaml
|
||||
./dag-engine list ~/dagu/dags/
|
||||
|
||||
# Servidor web (production: gestionado por dag_engine.service systemd user unit)
|
||||
./dag-engine server --port 8090 --dags-dir apps/dag_engine/dags_migrated/ --scheduler
|
||||
# Servidor web
|
||||
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
|
||||
# Browser: http://localhost:8090
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
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.
|
||||
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
|
||||
Puerto por defecto 8090 (mismo que Dagu).
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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]
|
||||
@@ -1,51 +0,0 @@
|
||||
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]
|
||||
@@ -1,438 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-53
@@ -156,49 +156,22 @@ 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),
|
||||
FunctionID: stepFunctionID,
|
||||
Status: "running",
|
||||
StartedAt: &now,
|
||||
ID: stepID,
|
||||
RunID: runID,
|
||||
StepName: stepName(step),
|
||||
Status: "running",
|
||||
StartedAt: &now,
|
||||
})
|
||||
|
||||
// Build environment.
|
||||
env := buildStepEnv(dag, step, daguEnvPath, outputs)
|
||||
|
||||
// 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)
|
||||
// Determine command.
|
||||
command := step.Command
|
||||
if command == "" && step.Script != "" {
|
||||
command = step.Script
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
|
||||
return nil
|
||||
@@ -209,15 +182,11 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
|
||||
command = resolveStepRefs(command, outputs)
|
||||
mu.Unlock()
|
||||
|
||||
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
|
||||
// so `fn` resolves registry.db correctly via go.mod walk-up.
|
||||
// Determine working directory.
|
||||
dir := step.Dir
|
||||
if dir == "" {
|
||||
dir = dag.WorkingDir
|
||||
}
|
||||
if dir == "" && stepFunctionID != "" {
|
||||
dir = fnRegistryRoot
|
||||
}
|
||||
|
||||
shell := step.Shell
|
||||
if shell == "" {
|
||||
@@ -357,15 +326,14 @@ 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"`
|
||||
LastRuns []store.DagRun `json:"last_runs,omitempty"` // 5 mas recientes (mas reciente primero)
|
||||
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"`
|
||||
}
|
||||
|
||||
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
|
||||
@@ -411,11 +379,10 @@ func (e *Executor) ListDAGs() ([]DagInfo, error) {
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
// Attach last 5 runs (most recent first).
|
||||
runs, _, _ := e.store.ListRuns(dag.Name, 5, 0)
|
||||
// Attach last run info.
|
||||
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
|
||||
if len(runs) > 0 {
|
||||
info.LastRun = &runs[0]
|
||||
info.LastRuns = runs
|
||||
}
|
||||
|
||||
dags = append(dags, info)
|
||||
|
||||
@@ -9,63 +9,12 @@ import {
|
||||
Paper,
|
||||
Alert,
|
||||
Loader,
|
||||
CopyButton,
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
Code,
|
||||
} from "@mantine/core";
|
||||
import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react";
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
import { getRun } from "../api";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { StepTimeline } from "../components/StepTimeline";
|
||||
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");
|
||||
}
|
||||
import type { RunDetail as RunDetailType } from "../types";
|
||||
|
||||
export function RunDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -151,41 +100,6 @@ 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,7 +5,6 @@ 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 (
|
||||
@@ -29,21 +28,19 @@ 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.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/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/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+10
-18
@@ -111,50 +111,44 @@ 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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
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/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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/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.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/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
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/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=
|
||||
@@ -172,5 +166,3 @@ 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,
|
||||
"recent_runs": runs,
|
||||
"info": info,
|
||||
"dag": dag,
|
||||
"validation": validation,
|
||||
"runs": runs,
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -286,7 +286,6 @@ 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
|
||||
@@ -304,7 +303,7 @@ func cmdServer(args []string) {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
RegisterAPI(mux, executor, scheduler, dagRunHub, feFS)
|
||||
RegisterAPI(mux, executor, scheduler, feFS)
|
||||
|
||||
handler := corsMiddleware(loggingMiddleware(mux))
|
||||
|
||||
|
||||
@@ -2,43 +2,15 @@ package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//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
|
||||
}
|
||||
//go:embed migrations/001_init.sql
|
||||
var migrationSQL string
|
||||
|
||||
// DB wraps a SQLite connection for DAG run persistence.
|
||||
type DB struct {
|
||||
@@ -52,7 +24,7 @@ func Open(path string) (*DB, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: open %s: %w", path, err)
|
||||
}
|
||||
if err := applyMigrations(conn); err != nil {
|
||||
if _, err := conn.Exec(migrationSQL); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("store: migrate: %w", err)
|
||||
}
|
||||
@@ -64,24 +36,18 @@ 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 `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"`
|
||||
ID string
|
||||
DagName string
|
||||
DagPath string
|
||||
Status string
|
||||
Trigger string
|
||||
StartedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
Error string
|
||||
}
|
||||
|
||||
// CreateRun inserts a new run record.
|
||||
@@ -157,18 +123,17 @@ 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 `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"`
|
||||
ID string
|
||||
RunID string
|
||||
StepName string
|
||||
Status string
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
StartedAt *time.Time
|
||||
FinishedAt *time.Time
|
||||
DurationMs int64
|
||||
Error string
|
||||
}
|
||||
|
||||
// InsertStepResult inserts a new step result.
|
||||
@@ -183,9 +148,9 @@ func (db *DB) InsertStepResult(r *DagStepResult) error {
|
||||
finishedAt = &s
|
||||
}
|
||||
_, err := db.conn.Exec(
|
||||
`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,
|
||||
`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,
|
||||
startedAt, finishedAt, r.DurationMs, r.Error,
|
||||
)
|
||||
return err
|
||||
@@ -208,7 +173,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, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
|
||||
`SELECT id, run_id, step_name, 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 {
|
||||
@@ -220,7 +185,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.FunctionID, &r.Status, &r.ExitCode,
|
||||
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode,
|
||||
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+1
-15
@@ -2,7 +2,6 @@
|
||||
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:
|
||||
@@ -31,11 +30,8 @@ uses_types:
|
||||
- dag_types_cpp_gfx
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
dir_path: "apps/shaders_lab"
|
||||
dir_path: "cpp/apps/shaders_lab"
|
||||
repo_url: ""
|
||||
icon:
|
||||
phosphor: "palette"
|
||||
accent: "#ea580c"
|
||||
---
|
||||
|
||||
## Arquitectura
|
||||
@@ -106,13 +102,3 @@ 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,21 +6,16 @@
|
||||
scan_secrets_in_dirty() {
|
||||
local repo_dir="${1:-.}"
|
||||
|
||||
# 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
|
||||
if [[ ! -d "$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.
|
||||
# Excluye extensiones de codigo (sh/go/py/ts/md/etc) para no marcar el
|
||||
# propio scanner ni docs que hablen de "secret"/"token".
|
||||
# y filtrar por patron de secret en el nombre del archivo
|
||||
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.1.0"
|
||||
version: "1.0.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,8 +63,3 @@ 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,38 +30,12 @@ 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 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
|
||||
# --- 4. Copiar .exe al top level ---
|
||||
cp -v "$exe_src" "$dest/"
|
||||
|
||||
# --- 5. DLLs al top level (Windows DLL search convention) ---
|
||||
find "$build_win/apps/$app" -maxdepth 1 -type f -name '*.dll' \
|
||||
|
||||
@@ -17,9 +17,7 @@ git_hook_audit_app_drift() {
|
||||
echo "ERROR: repo_dir required" >&2
|
||||
return 2
|
||||
fi
|
||||
# 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
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
echo "ERROR: $repo_dir is not a git repo" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
@@ -3,7 +3,7 @@ name: gradle_run
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "gradle_run(project_dir: string, task...: string) -> int"
|
||||
description: "Wrapper canonico para invocar gradlew Android en WSL2 con JDK 17 + ANDROID_HOME validados."
|
||||
@@ -24,7 +24,7 @@ tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/gradle_run.sh"
|
||||
notes: "Las demas funciones gradle_* lo sourcean. Reutiliza patron de adb_wsl_bash_infra para ser source-able+ejecutable. Cubre tanto SDK Linux (~/Android/Sdk via install_android_sdk) como SDK Windows (/mnt/c/...) montado en WSL."
|
||||
notes: "Las demas funciones gradle_* lo sourcean. Reutiliza patron de adb_wsl_bash_infra para ser source-able+ejecutable. Cubre SDK Linux en $HOME/android-sdk (install_android_sdk_bash_infra), $HOME/Android/Sdk (Android Studio), y SDK Windows (/mnt/c/...) montado en WSL."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
@@ -50,10 +50,12 @@ Si no esta fijado en el entorno, busca en orden:
|
||||
Si ninguno existe → error en stderr y `return 1`.
|
||||
|
||||
### ANDROID_HOME
|
||||
Si no esta fijado:
|
||||
1. Intenta `$HOME/Android/Sdk` (SDK Linux via `install_android_sdk_bash_infra`)
|
||||
2. Si no existe, intenta `$ANDROID_SDK_WIN` (SDK Windows montado en `/mnt/c/...`)
|
||||
3. Si ninguno, lo deja vacio — gradle mostrara el error adecuado para builds JVM puros
|
||||
Si no esta fijado, busca en orden (requiere que el directorio tenga `platform-tools/`):
|
||||
1. `$HOME/android-sdk` — default de `install_android_sdk_bash_infra` (lowercase)
|
||||
2. `$HOME/Android/Sdk` — default de Android Studio en Linux
|
||||
3. `$ANDROID_SDK_WIN` (o `/mnt/c/Users/$USER/AppData/Local/Android/Sdk`) — SDK Windows montado en WSL2
|
||||
|
||||
Si ninguno existe con `platform-tools/`, lo deja vacio — gradle mostrara el error adecuado para builds JVM puros
|
||||
|
||||
## Exit codes
|
||||
|
||||
@@ -69,4 +71,8 @@ Si no esta fijado:
|
||||
Source-able y ejecutable directo. Al sourcear, el caller importa la funcion `gradle_run` sin ejecutarla. Al ejecutar directamente, delega `"$@"` a `gradle_run`.
|
||||
|
||||
No exporta `JAVA_HOME`/`ANDROID_HOME` al entorno del shell padre — los variables se pasan solo al subshell de gradlew para evitar contaminar el entorno.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-05-15) — ANDROID_HOME detection order: prioriza `$HOME/android-sdk` (install_android_sdk default) sobre `$HOME/Android/Sdk`; requiere platform-tools/ presente; anade WSL2 Windows path como fallback explicito (issue 0076)
|
||||
---
|
||||
|
||||
@@ -44,16 +44,25 @@ gradle_run() {
|
||||
fi
|
||||
|
||||
# ---- Resolver ANDROID_HOME ---------------------------------------------
|
||||
# Orden de busqueda (de mas probable a menos para entorno Linux/WSL2):
|
||||
# 1. $HOME/android-sdk — instalado por install_android_sdk_bash_infra (default)
|
||||
# 2. $HOME/Android/Sdk — ruta de Android Studio en Linux
|
||||
# 3. $ANDROID_SDK_WIN — SDK Windows montado en WSL2 via /mnt/c/...
|
||||
# Solo se acepta un candidato si tiene platform-tools/, no solo el directorio raiz.
|
||||
local android_home="${ANDROID_HOME:-}"
|
||||
if [[ -z "$android_home" ]]; then
|
||||
local _default_linux="$HOME/Android/Sdk"
|
||||
if [[ -d "$_default_linux" ]]; then
|
||||
android_home="$_default_linux"
|
||||
elif [[ -n "${ANDROID_SDK_WIN:-}" && -d "${ANDROID_SDK_WIN}" ]]; then
|
||||
# SDK Windows montado en WSL via /mnt/c/...
|
||||
android_home="${ANDROID_SDK_WIN}"
|
||||
fi
|
||||
unset _default_linux
|
||||
local _sdk_candidates=(
|
||||
"$HOME/android-sdk"
|
||||
"$HOME/Android/Sdk"
|
||||
"${ANDROID_SDK_WIN:-/mnt/c/Users/$USER/AppData/Local/Android/Sdk}"
|
||||
)
|
||||
for _candidate in "${_sdk_candidates[@]}"; do
|
||||
if [[ -d "$_candidate" && -d "$_candidate/platform-tools" ]]; then
|
||||
android_home="$_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
unset _sdk_candidates _candidate
|
||||
fi
|
||||
|
||||
# ANDROID_HOME puede quedar vacio si no hay SDK instalado; gradle mostrara
|
||||
|
||||
@@ -3,7 +3,7 @@ name: launch_cpp_app_windows
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.1.0"
|
||||
version: "1.0.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,13 +68,3 @@ 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,8 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# launch_cpp_app_windows v1.1.0 — Lanza un .exe en Windows desde WSL2 via PowerShell.
|
||||
# launch_cpp_app_windows — Lanza un .exe en Windows desde WSL2 via cmd.exe /c start.
|
||||
# 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:-}"
|
||||
@@ -28,18 +26,10 @@ 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 \
|
||||
"\$env:FN_REGISTRY_ROOT='$win_root'; \$env:FN_REGISTRY_ROOT_WSL='$linux_root'; Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
|
||||
"Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
|
||||
>/dev/null 2>&1
|
||||
|
||||
local ts
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/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.1.0"
|
||||
version: "1.0.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 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]
|
||||
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]
|
||||
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 apps/<X>/, 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 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,13 +44,4 @@ APP_DIR="$(echo "$resolved" | cut -f2)"
|
||||
|
||||
## Notas
|
||||
|
||||
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`).
|
||||
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.
|
||||
|
||||
@@ -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 apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/.
|
||||
# Con arg: usa el nombre directamente y busca en las tres ubicaciones.
|
||||
# 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.
|
||||
# Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0.
|
||||
# Error: lista apps disponibles en stderr + exit 1.
|
||||
|
||||
@@ -9,28 +9,18 @@ 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%%/*}"
|
||||
@@ -43,7 +33,12 @@ resolve_cpp_app_dir() {
|
||||
echo "ERROR: no se pudo deducir la app desde el directorio actual." >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
_list_cpp_apps >&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
|
||||
echo "" >&2
|
||||
echo "Uso: resolve_cpp_app_dir <app_name>" >&2
|
||||
return 1
|
||||
@@ -52,17 +47,12 @@ resolve_cpp_app_dir() {
|
||||
# --- Buscar directorio real ---
|
||||
local app_dir=""
|
||||
|
||||
# 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
|
||||
# Primero: cpp/apps/<X>
|
||||
if [ -d "$root/cpp/apps/$app_arg" ]; then
|
||||
app_dir="$root/cpp/apps/$app_arg"
|
||||
fi
|
||||
|
||||
# Tercero: projects/*/apps/<X> (primer match)
|
||||
# Segundo: projects/*/apps/<X> (primer match)
|
||||
if [ -z "$app_dir" ]; then
|
||||
for cand in "$root"/projects/*/apps/"$app_arg"; do
|
||||
if [ -d "$cand" ]; then
|
||||
@@ -73,10 +63,15 @@ resolve_cpp_app_dir() {
|
||||
fi
|
||||
|
||||
if [ -z "$app_dir" ]; then
|
||||
echo "ERROR: no se encuentra app '$app_arg' en apps/, cpp/apps/ ni en projects/*/apps/" >&2
|
||||
echo "ERROR: no se encuentra app '$app_arg' en cpp/apps/ ni en projects/*/apps/" >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
_list_cpp_apps >&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
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/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: "1.1.0"
|
||||
version: "0.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, cpp-tables]
|
||||
tags: [cpp, imgui, scaffold, pipeline, bash, launcher]
|
||||
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) + 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
|
||||
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)
|
||||
```
|
||||
|
||||
Y ademas:
|
||||
@@ -66,31 +66,9 @@ 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. 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`.
|
||||
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`.
|
||||
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 (apps/<name>/, issue 0096).
|
||||
# Por defecto domain=tools, sin proyecto (cpp/apps/<name>/).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -55,7 +55,7 @@ init_cpp_app() {
|
||||
fi
|
||||
rel_dir="projects/$project/apps/$name"
|
||||
else
|
||||
rel_dir="apps/$name"
|
||||
rel_dir="cpp/apps/$name"
|
||||
fi
|
||||
abs_dir="$FN_ROOT/$rel_dir"
|
||||
|
||||
@@ -69,11 +69,9 @@ init_cpp_app() {
|
||||
# ---------- main.cpp ----------
|
||||
cat > "$abs_dir/main.cpp" <<EOF
|
||||
#include <imgui.h>
|
||||
#include "app_base.h"
|
||||
#include "core/panel_menu.h"
|
||||
#include "framework/app_base.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;
|
||||
@@ -92,11 +90,6 @@ 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*/) {
|
||||
@@ -122,12 +115,6 @@ 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()
|
||||
@@ -148,20 +135,7 @@ lang: cpp
|
||||
domain: $domain
|
||||
description: "$desc"
|
||||
tags: $tags_yaml
|
||||
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_functions: []
|
||||
uses_types: []
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
@@ -201,14 +175,11 @@ if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
|
||||
endif()
|
||||
EOF
|
||||
else
|
||||
local upper
|
||||
upper="$(echo "$name" | tr '[:lower:]' '[:upper:]')"
|
||||
cat >> "$cpp_cmake" <<EOF
|
||||
|
||||
# --- $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)
|
||||
# --- $name ---
|
||||
if(EXISTS \${CMAKE_CURRENT_SOURCE_DIR}/apps/$name/CMakeLists.txt)
|
||||
add_subdirectory(apps/$name)
|
||||
endif()
|
||||
EOF
|
||||
fi
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/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,17 +3,16 @@ name: redeploy_cpp_app_windows
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.1.0"
|
||||
version: "1.0.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 e incluye refresh del icon cache del shell."
|
||||
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify."
|
||||
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
|
||||
@@ -48,7 +47,6 @@ 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,7 +7,6 @@ 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=""
|
||||
@@ -64,12 +63,6 @@ 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
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,132 +0,0 @@
|
||||
#!/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
|
||||
+1
-255
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
@@ -40,8 +39,6 @@ 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":
|
||||
@@ -62,14 +59,6 @@ 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()
|
||||
@@ -87,7 +76,6 @@ 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
|
||||
@@ -96,10 +84,6 @@ 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)
|
||||
@@ -139,11 +123,6 @@ 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 {
|
||||
@@ -189,21 +168,10 @@ 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(map[string]any{
|
||||
"conformance": audits,
|
||||
"table_migration": tableAudits,
|
||||
})
|
||||
emit(audits)
|
||||
return
|
||||
}
|
||||
|
||||
// Conformance section.
|
||||
bad := 0
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "STATUS\tAPP\tISSUES")
|
||||
@@ -219,31 +187,6 @@ 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) {
|
||||
@@ -301,58 +244,6 @@ 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 {
|
||||
@@ -581,148 +472,3 @@ 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-37
@@ -143,8 +143,8 @@ func cmdIndex() {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
for _, e := range result.ValidationErrors {
|
||||
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
|
||||
}
|
||||
@@ -420,42 +420,10 @@ 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)
|
||||
@@ -572,9 +540,6 @@ 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,7 +27,6 @@ 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"`
|
||||
}
|
||||
@@ -38,7 +37,6 @@ 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 {
|
||||
@@ -102,7 +100,6 @@ 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
|
||||
@@ -115,7 +112,6 @@ func syncPushPull() {
|
||||
Analysis: analysis,
|
||||
Projects: projects,
|
||||
Vaults: vaults,
|
||||
Modules: modules,
|
||||
Proposals: proposals,
|
||||
Locations: locations,
|
||||
}
|
||||
@@ -207,14 +203,6 @@ 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) {
|
||||
@@ -341,7 +329,6 @@ func syncStatus() {
|
||||
analysis, _ := db.AllAnalysis()
|
||||
projects, _ := db.ListAllProjects()
|
||||
vaults, _ := db.AllVaults()
|
||||
modules, _ := db.AllModules()
|
||||
proposals, _ := db.AllProposals()
|
||||
locs, _ := db.ListAllPcLocations()
|
||||
|
||||
@@ -350,7 +337,6 @@ 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))
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
||||
+31
-187
@@ -1,9 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
if(WIN32)
|
||||
project(fn_registry_cpp LANGUAGES C CXX RC)
|
||||
else()
|
||||
project(fn_registry_cpp LANGUAGES C CXX)
|
||||
endif()
|
||||
project(fn_registry_cpp LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -240,75 +236,10 @@ endif()
|
||||
set(FN_CPP_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "fn_registry cpp root")
|
||||
|
||||
function(add_imgui_app target)
|
||||
# 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})
|
||||
add_executable(${target} ${ARGN})
|
||||
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)
|
||||
@@ -343,29 +274,14 @@ 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.
|
||||
|
||||
# --- 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)
|
||||
# --- Demo app ---
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt)
|
||||
add_subdirectory(apps/chart_demo)
|
||||
endif()
|
||||
|
||||
# --- 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)
|
||||
# --- Shaders Lab ---
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt)
|
||||
add_subdirectory(apps/shaders_lab)
|
||||
endif()
|
||||
|
||||
# --- Lua 5.4 vendored (para playground tables / DSL formulas) ---
|
||||
@@ -373,39 +289,30 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
|
||||
add_subdirectory(vendor/lua)
|
||||
endif()
|
||||
|
||||
# --- 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)
|
||||
# --- 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)
|
||||
endif()
|
||||
|
||||
# --- 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)
|
||||
# --- 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)
|
||||
endif()
|
||||
|
||||
# --- AltSnap viewport-jitter regression test (lives in apps/) ---
|
||||
if(NOT DEFINED _AJT_DIR)
|
||||
set(_AJT_DIR ${CMAKE_SOURCE_DIR}/../apps/altsnap_jitter_test)
|
||||
# --- 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)
|
||||
endif()
|
||||
if(EXISTS ${_AJT_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_AJT_DIR} ${CMAKE_BINARY_DIR}/apps/altsnap_jitter_test)
|
||||
|
||||
# --- 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)
|
||||
endif()
|
||||
|
||||
# --- gamedev stack (SDL3 + sokol_gfx + miniaudio, issue 0072) ---
|
||||
@@ -421,17 +328,11 @@ 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(NOT DEFINED _ES_DIR)
|
||||
set(_ES_DIR ${CMAKE_SOURCE_DIR}/../apps/engine_smoke)
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/engine_smoke/CMakeLists.txt)
|
||||
add_subdirectory(apps/engine_smoke)
|
||||
endif()
|
||||
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)
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/runtime_test/CMakeLists.txt)
|
||||
add_subdirectory(apps/runtime_test)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -478,60 +379,3 @@ 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
+1
Submodule cpp/apps/altsnap_jitter_test added at 6e52b658a3
Submodule cpp/apps/chart_demo deleted from 026f514bb7
@@ -0,0 +1,22 @@
|
||||
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()
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,89 @@
|
||||
#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
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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
+1
Submodule cpp/apps/engine_smoke added at bed33856e7
@@ -0,0 +1,110 @@
|
||||
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()
|
||||
@@ -0,0 +1,159 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
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.
|
After Width: | Height: | Size: 966 B |
@@ -0,0 +1,173 @@
|
||||
// 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
|
||||
@@ -0,0 +1,34 @@
|
||||
#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
|
||||
@@ -0,0 +1,76 @@
|
||||
#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
|
||||
@@ -0,0 +1,22 @@
|
||||
#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
|
||||
@@ -0,0 +1,56 @@
|
||||
#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
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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
|
||||
@@ -0,0 +1,249 @@
|
||||
// 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
|
||||
@@ -0,0 +1,447 @@
|
||||
#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
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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
|
||||
@@ -0,0 +1,443 @@
|
||||
#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
|
||||
@@ -0,0 +1,243 @@
|
||||
#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
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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
|
||||
@@ -0,0 +1,208 @@
|
||||
// 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
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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
|
||||
@@ -0,0 +1,279 @@
|
||||
// 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
|
||||
@@ -0,0 +1,211 @@
|
||||
#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
|
||||
@@ -0,0 +1,230 @@
|
||||
// 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
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user