79 Commits

Author SHA1 Message Date
egutierrez 11e6e27ad1 fix(auto): fix secret_store.md YAML — returns/error_type/imports format
- returns: [] (was empty string)
- error_type: error_go_core (was empty, required for impure)
- imports: [list] (was string)
- Removed stale id: field (auto-computed from filename)
- Added output: field for params_schema

fn index now clean: 1324 functions, 45 apps

Co-Authored-By: fn-orquestador <noreply@fn-registry.local>
2026-05-22 21:48:21 +02:00
egutierrez a59b12d467 feat(auto): construir iter 1 — add secret_store_cpp_infra registry function
DPAPI Windows + XOR Linux fallback para almacenar credentials sensibles
en SQLite local. Usado por agents_dashboard para cifrar apikeys.
Incluye encrypt/decrypt/is_strong + base64 helpers.

Issue: 0129
Co-Authored-By: fn-constructor <noreply@fn-registry.local>
2026-05-22 21:42:44 +02:00
egutierrez fe4320af89 chore(auto): construir iter 1 — scaffold agents_dashboard + register in CMakeLists
- init_cpp_app_bash_pipelines scaffold:
  projects/element_agents/apps/agents_dashboard/{main.cpp,CMakeLists.txt,app.md}
- git init dentro del sub-repo (apps_subrepo.md regla)
- Registrado en cpp/CMakeLists.txt (add_subdirectory via _AGENTS_DASHBOARD_DIR)

Co-Authored-By: fn-constructor <noreply@fn-registry.local>
2026-05-22 21:37:06 +02:00
egutierrez f71e0f4c9a chore: remove kanban_cpp app
Gitea repo dataforge/kanban_cpp archived (read-only).
Local apps/kanban_cpp/ deleted, CMake subdir registration removed.
registry.db entry + pc_locations row purged (regenerable via fn index +
manual delete since indexer upserts but does not purge orphaned apps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:30:47 +02:00
egutierrez 46b4385331 feat(issues): 0128 agents_and_robots HTTP API + 0129 agents_dashboard C++ ImGui
0128 (backend, blocks 0129): HTTP daemon en cmd/launcher con apikey Bearer auth, SSE pubsub in-memory para status+logs, TLS via Traefik en agents.organic-machine.com, systemd Restart=always. Scope v0.1 lean: list/start/stop/restart/logs SSE. Send-message + config-edit en v0.2.

0129 (frontend): C++ ImGui agents_dashboard en projects/element_agents/apps/. Panels Connection/Agents/Logs/Status. Persistencia local cifrada DPAPI. Depende de 0128.

Ambos issues con dod_evidence_schema completo (9 + 9 items).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:11:30 +02:00
egutierrez 580238b32e feat(infra): auto-commit con 8 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:38:16 +02:00
egutierrez ed767360c1 chore: auto-commit (1 archivos)
- cpp/apps/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:17:04 +02:00
egutierrez 5bac05ce13 refactor(commands): merge /autonomous-task → /autopilot v2
Doble entrada confusa (incidente 2026-05-19 piloto 0121b: cwd
mutation por Path B inline causo commit a branch incorrecta).

Cambios:
- .claude/commands/autopilot.md: v2 simplificado. SOLO pre-flight
  DoD check + delegate fn-orquestador. Sin Path A/B/C inline.
  Self-Q&A migrado al orquestador. Cero cwd mutation.
- .claude/commands/autonomous-task.md: DEPRECADO. Sustitucion 1:1.
  Sigue funcionando como debug primitive sin DoD check.
- dev/issues/0123: revision — eliminar /flow run y /fix-flow (absorbidos
  por /autopilot v2). Mantener fn-meta-orquestador + fn-priorizador
  + fn doctor issues/flows. Anadir tarea: dar a fn-orquestador soporte
  task_type=flow.

Preferencia humano: 1 sola entrada autopilot, "modo que entra y sigue".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:52:19 +02:00
egutierrez d0ceea6f3d done(0121b): fn doctor e2e-coverage funcionando — 42.22% coverage actual
task_run task_d285372493cce2e6 converged 1 iter / ~4 min.
PR https://gitea-.../dataforge/fn_registry/pulls/3 mergeado.

Verificado en master:
  total=45 with_checks=19 coverage=42.22%
  21 apps con propuesta lista en dev/proposals_e2e_checks_0121/ esperando aplicacion (0121c).
  5 apps sin propuesta aun (wave 4 pendiente).

Desbloquea: 0121c (apply N PRs add_e2e_check).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:48:15 +02:00
dataforge 0f905b78e0 merge(0121b): fn doctor e2e-coverage
task_run task_d285372493cce2e6 converged 1 iter / 4 min. 4/4 acceptance.

functions/infra/audit_e2e_coverage.go + .md + _test.go
types/infra/e2e_coverage_report.md
cmd/fn/doctor.go (subcmd e2e-coverage)

Cierra: dev/issues/0121b-fn-doctor-e2e-coverage.md
2026-05-18 23:47:32 +00:00
egutierrez 5c7ff8d761 feat(0121b): audit_e2e_coverage_go_infra + fn doctor e2e-coverage subcmd
- Crea functions/infra/audit_e2e_coverage.go: AuditE2ECoverage(roots) escanea
  app.md recursivamente, detecta e2e_checks: en frontmatter, retorna
  E2ECoverageReport{total, with_checks, missing, coverage_pct}.
- Crea functions/infra/e2e_coverage_report.go: tipo E2ECoverageReport con
  JSON tags (total, with_checks, missing, coverage_pct).
- Crea types/infra/e2e_coverage_report.md: metadata del tipo para registry.
- Crea functions/infra/audit_e2e_coverage.md: documentacion self-contained
  con Ejemplo, Cuando usarla, Gotchas.
- Crea functions/infra/audit_e2e_coverage_test.go: 3 tests (empty, all-covered,
  partial) — todos pasan.
- Edita cmd/fn/doctor.go: agrega case "e2e-coverage" -> doctorE2ECoverage().
  Output text (tabla tabwriter + lista de apps missing) y --json (E2ECoverageReport).

Acceptance verificado:
  fn doctor e2e-coverage --json -> {total, with_checks, missing, coverage_pct} OK
  fn doctor e2e-coverage        -> tabla text OK
  go test ./functions/infra/... -> 3/3 PASS
  fn show audit_e2e_coverage_go_infra -> indexada OK

task_run: task_d285372493cce2e6 iter 1

Co-authored-by: fn-orquestador <noreply@fn-registry>
2026-05-19 01:45:54 +02:00
egutierrez 138f4b2713 chore: auto-commit (9 archivos)
- .claude/commands/autopilot.md
- dev/proposals_e2e_checks_0121/altsnap_jitter_test.yaml
- dev/proposals_e2e_checks_0121/app_hub_launcher.yaml
- dev/proposals_e2e_checks_0121/element_matrix_chat.yaml
- dev/proposals_e2e_checks_0121/footprint_geo_stack.yaml
- dev/proposals_e2e_checks_0121/metabase_registry.yaml
- dev/proposals_e2e_checks_0121/script_navegador.yaml
- dev/proposals_e2e_checks_0121/services_monitor.yaml
- dev/proposals_e2e_checks_0121/tables_qa.yaml

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:41:49 +02:00
egutierrez 25425a5fd6 chore: auto-commit (1 archivos)
- .claude/commands/autopilot.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:37:33 +02:00
egutierrez 89441539fa docs(issues): 4 issues de deuda detectada lateral en 0121a
Origen: fn-recopilador design-e2e descubrio 6 bugs durante el design
de propuestas e2e_checks. Agrupados en 4 issues:

- 0124 EPIC dag_engine cleanup (registry.db huerfana + Mantine drift
       + --migrate-only flag — 3 sub-tareas)
- 0125 deploy_server: anadir --db a cmdServe
- 0126 pipeline_launcher: aplicar migracion 003_logs
- 0127 docker_tui: go.work path absoluto rompe portabilidad

Todos relacionados con 0121a. Pueden ser candidatos a /autonomous-task
o /autopilot dependiendo del scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:36:53 +02:00
egutierrez 1d3d2f43b3 feat(0121a): wave 2 e2e_checks proposals (8 apps) + README updated
8 fn-recopilador design-e2e paralelos:
- services_api      (Go service, schema custom operations.db)
- registry_mcp      (Go stdio MCP, JSON-RPC handshake test)
- sqlite_api        (Go service read-only HTTP, query_endpoint)
- registry_dashboard (C++ ImGui, NO Go+React como yo supuse)
- primitives_gallery (C++ build gate de toda API C++ del registry, 44 .cpp)
- pipeline_launcher (Go TUI bubbletea)
- docker_tui        (Go TUI + go-duckdb)
- fn_match          (subcmd ./fn, hook helper, fuzzy match)

13/26 apps cubiertas. README documenta:
- 6 bugs/drift descubiertos lateral (dag_engine x3, deploy_server,
  pipeline_launcher, docker_tui).
- 3 correcciones de mi prompt (yo asumi stacks incorrectos).
- Hallazgos arquitectonicos (primitives_gallery = build gate C++).

Pendiente wave 3 (13 apps) + 0121b + 0121c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:43:09 +02:00
egutierrez 2effb688b0 feat(0121a): wave 1 e2e_checks proposals (5 apps)
5 fn-recopilador design-e2e paralelos sobre mix de stacks:
- deploy_server (Go service)
- registry_api  (Go service + FTS5)
- shaders_lab   (C++ ImGui)
- auto_metabase (Python CLI)
- dag_engine    (Go scheduler + react/vite frontend)

Total: 24 checks propuestos. Bloques YAML listos para pegar al
frontmatter de cada app.md tras revision humana.

Advertencias laterales en README.md:
- dag_engine: registry.db huerfana + pnpm build roto + falta --migrate-only
- deploy_server: --db flag no expuesto en cmdServe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:32:55 +02:00
egutierrez eb30074792 chore: auto-commit (8 archivos)
- .claude/rules/registry_calls.md
- apps/dag_engine/README.md
- apps/dag_engine/app.md
- docs/capabilities/INDEX.md
- docs/capabilities/systemd.md
- docs/execution_standard.md
- dev/proposals_e2e_checks_0121/
- docs/capabilities/backends.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:31:30 +02:00
egutierrez f8efb7d177 split(0121): epic + 3 sub-issues — design/doctor/apply
0121 era scope demasiado grande para 1 orquestador (batch + new function +
new subcmd + N edits). Split:

- 0121a chore: design-e2e batch (Claude orquesta N fn-recopilador paralelos)
- 0121b feature: audit_e2e_coverage + fn doctor e2e-coverage subcmd
       (tipo feature_app_simple, apto /autonomous-task)
- 0121c chore: aplicar propuestas via N /autonomous-task add_e2e_check

Cada hijo tiene Acceptance verificable + tipo orquestador declarado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:22:23 +02:00
egutierrez f428f2c82a done(0120): orquestador piloto verde — PR mergeado en chart_demo
task_run task_b3069559f34415c3 converged en 2 iter / 2m28s.
PR https://gitea-.../dataforge/chart_demo/pulls/1 mergeado a master.

Desbloquea: 0121, 0122, 0123.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:20:38 +02:00
egutierrez f36d091704 docs(0120): hallazgos piloto + regla sub-repos/Gitea API en autonomous_loop
Piloto 0120 convergio en 2 iter (2m28s). PR creado en
dataforge/chart_demo/pulls/1 (no en dataforge/fn_registry — sub-repo).

Anadido a autonomous_loop.md:
- Seccion "Sub-repos vs worktree padre": orquestador opera en sub-repo
  cuando issue toca apps/, projects/*/apps/, cpp/apps/ o analysis/.
- Seccion "Gitea API vs gh": gh auth es smoke, real es curl + pass token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:19:30 +02:00
egutierrez 938853d268 fix(0120): R1+R4 del dry-run — check binary_exists + DB path correcto
R1: fn::run_app no parsea argv. Cambio self_test (--self-test inexistente)
por check estructural test -f binary. Detectado por fn-orquestador dry-run.

R4: schema real de task_runs usa task_id (no issue_id) y DB vive en
apps/deploy_server/operations.db (no agent_runner_api).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:00:11 +02:00
egutierrez b31ea70771 docs(plan): issues 0120-0123 — mejora workflow subagentes
Plan en 4 olas para cerrar gaps detectados en revision critica:
- 0120 piloto fn-orquestador (chart_demo e2e_checks)
- 0121 cobertura e2e_checks masiva (fn-recopilador batch)
- 0122 fn-revisor + auto-apply ampliado (desbloquea fase 5)
- 0123 /flow run + fn-meta-orquestador + fn-priorizador

Dep-chain: 0120 -> 0121 -> 0122 -> 0123. Cada uno con
Acceptance verificable programaticamente para que /autonomous-task
pueda converger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:41:02 +02:00
egutierrez 2e5c630d38 chore: baseline pre-piloto 0120 — apps_subrepo rule + http/sse hardening
WIP previo al lanzamiento de fn-orquestador piloto.
Commit como baseline para que /autonomous-task 0120 arranque con master limpio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:40:52 +02:00
egutierrez c52846d475 feat(cpp/core): sse_client_cpp_core — SSE client con reconnect + Last-Event-ID
Cliente Server-Sent Events C++ reusable (fn_sse::Client) con background
thread, exponential backoff, Last-Event-ID y stop() que no bloquea.

Implementacion clave: fork+execvp curl directamente (sin /bin/sh wrapper)
para tener el PID real del proceso curl en curl_pid_, lo que permite que
stop() → kill(SIGTERM) → fgets NULL → join() funcione sin bloqueo.

4 tests (Catch2): connect_and_receive_3_events, parse_event_field,
reconnect_on_disconnect, stop_kills_thread. Fixture Python SSE con
/health probe via http_request_cpp_core.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:59:50 +02:00
egutierrez b5affae68c merge issue/0119: kanban_cpp issues/flows sync layer 2026-05-18 18:57:11 +02:00
agent 5b4452b9fe feat(0119): kanban_cpp issues/flows sync layer
Closes issue 0119. Implementation lives in the kanban_cpp sub-repo
(apps/kanban_cpp/backend/). See sub-repo commit 0b93a98 for details.

DoD:
- [x] issues_source.go parses frontmatter via yaml.v3
- [x] flows_source.go idem (distinct status->column mapping)
- [x] frontmatter_edit.go atomic PatchFrontmatterField
- [x] GET /api/boards/issues/cards (smoke: 87 cards)
- [x] GET /api/boards/flows/cards (smoke: 9 cards)
- [x] PATCH /api/boards/issues/cards/0119 status=en-curso (mtime change verified)
- [x] POST /api/boards/issues/cards/0119/launch -> 502 with suggestion
- [x] Tests *_test.go: 4 + 3 + 3 + 1 cache cases, all green
- [x] Cache 30s thread-safe (mutex)
- [x] Taxonomy 0103 respected — only canonical statuses accepted on PATCH

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:56:36 +02:00
egutierrez e0f8f3a068 merge issue/0116: skill_tree v2 Launch workflow boton + feature flag legacy_claude_fix 2026-05-18 18:47:36 +02:00
egutierrez b21e7587ad merge issue/0112: kanban_cpp C++ ImGui app + backend Go :8403 (sub-repo apps/kanban_cpp) 2026-05-18 18:47:31 +02:00
Egutierrez d4924f5cab feat(0112): register kanban_cpp app in cpp/CMakeLists.txt + close issue
Adds add_subdirectory block for apps/kanban_cpp (lives in apps/ per issue
0096). The app itself is a sub-repo (gitignored via apps/*/), with its own
git history and master branch initialized.

Six panels reuse registry: http_request_cpp_core, kpi_card_cpp_viz,
sparkline_cpp_viz, agent_runs_timeline_cpp_viz, dod_evidence_panel_cpp_viz.
Backend Go on :8403 (independent operations.db from kanban_web).
2026-05-18 18:46:50 +02:00
agent 853b3c0363 feat(skill_tree): Launch workflow via agent_runner_api + feature flag legacy_claude_fix (issue 0116)
- dev/feature_flags.json: anade 'legacy_claude_fix' (enabled:false,
  issue 0116). Default OFF — el flujo canonico ahora es 'Launch workflow'
  (POST :8486/api/runs); el boton 'Claude fix' legacy (terminal externa +
  claude --dangerously-skip-permissions) solo se renderiza si se activa
  el flag.
- Mueve dev/issues/0116-skill-tree-launch-workflow.md a completed/.

El codigo C++ del boton vive en el sub-repo dataforge/skill_tree
(apps/skill_tree, commit 9ee3be8).
2026-05-18 18:46:25 +02:00
egutierrez f164ef230f merge issue/0118: agent_runs_timeline C++ ImGui + helpers
# Conflicts:
#	cpp/tests/CMakeLists.txt
2026-05-18 18:33:02 +02:00
egutierrez ff255c9a3c merge issue/0117: dod_evidence_panel C++ ImGui + helpers 2026-05-18 18:32:35 +02:00
egutierrez 6c7f60fb6c merge issue/0115: agent_launch_worktree + agent_cleanup_worktree Go fns + capability group agents 2026-05-18 18:32:31 +02:00
egutierrez 75ac96a2d1 docs: cerrar issue 0118 — agent_runs_timeline_cpp_viz registrado
Funcion + helpers + tests + .md indexados.

- cpp/functions/viz/agent_runs_timeline.{h,cpp,md}
- cpp/functions/viz/agent_runs_timeline_helpers.{h,cpp}
- cpp/tests/test_agent_runs_timeline.cpp (17 cases, 53 assertions, PASS)
- cpp/tests/CMakeLists.txt: add_fn_test test_agent_runs_timeline

fn index: 1283 functions (+1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:53 +02:00
egutierrez da56085e74 docs(viz): agent_runs_timeline.md — frontmatter + self-doc
Issue 0118. Frontmatter completo (kind, lang, domain, version, purity,
signature, params, tags=agents/timeline/sse/imgui/viz/panel,
uses_functions=http_request_cpp_core, error_type, tested).

Secciones obligatorias por contrato self-doc:
- ## Ejemplo — wiring concreto con TimelineState g_state + lock + render
- ## Cuando usarla — dashboard cross-app de agentes
- ## Gotchas — SSE stub, ts en segundos, mutex obligatorio, no autoscroll,
  ImGuiSelectableFlags_AllowOverlap (rename desde AllowItemOverlap),
  app chip hex hardcoded, since_ts no determinista en tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:45 +02:00
egutierrez ecd864f2d3 test(viz): test_agent_runs_timeline — 17 cases / 53 assertions
Issue 0118. Catch2 coverage de los helpers puros:

- passes_filter: filtro vacio, filter por app, app+status combinado, since_ts
- filter_and_sort: orden started_at DESC, combina filtro + sort
- format_duration: running, segundos, minutos, horas, timestamps invertidos
- status_color_token: mapping conocido + fallback neutral
- status_icon_id: no-empty + distinto entre statuses
- app_chip_hex: apps conocidas + fallback gris

Mock fixture de 5 AgentRun mixtos en make_mock().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:37 +02:00
egutierrez a91ef5aace feat(viz): agent_runs_timeline ImGui panel + SSE stub
Issue 0118.

fn_viz::render_agent_runs_timeline(TimelineState&):
- Filtros: multi-select apps, multi-select statuses, Since (days).
- Connection badge (● green / ◐ amber / ○ red) por state.connection_status.
- Tabla 7 cols: status icon | app chip | issue/card | branch | dod badge |
  duration | started. Selectable SpanAllColumns dispara on_select callback.
- Footer: contadores per-status sobre el set completo.

Thread-safe: snapshot bajo runs_mutex al inicio del frame. SSE client
NO implementado — poll_sse_runs() es stub documentado en .md ## Gotchas.
Consumer puede usar http_request_cpp_core para polling fallback contra
GET /api/runs hasta que un endpoint /api/runs/stream estable aparezca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:29 +02:00
egutierrez c2bdc586a4 feat(viz): agent_runs_timeline helpers — pure filter/sort/format
Issue 0118.

Pure helpers in fn_viz::timeline namespace, free of ImGui:
- passes_filter / filter_and_sort  (multi-select app + status + since_ts)
- format_duration  (running | Ns | MmSSs | HhMMm | —)
- status_color_token / status_icon_id  (status → fn_tokens index / TI_*)
- app_chip_hex  (app id → accent hex, fallback gray)

Designed for unit-test isolation. Render layer (separate commit) consumes
these via agent_runs_timeline.h.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:17 +02:00
egutierrez 61a46e4b21 docs: cerrar issue 0117 (dod_evidence_panel C++ implementado)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:46 +02:00
egutierrez 3633d128aa docs(viz): frontmatter + self-doc para dod_evidence_panel (issue 0117)
Ejemplo lanzable con DodPanelState mock + Cuando usarla (HITL DoD
validation) + Gotchas (screenshot stub, URL no validada, log read
each-frame, callbacks pueden mutar state, frame ImGui activo
requerido). Tag agents para capability group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:37 +02:00
egutierrez 892ff4f789 test(viz): cubrir helpers de dod_evidence_panel (issue 0117)
7 test cases via Catch2: count_status (3 escenarios incl. unknown
status y missing_required), find_evidence (2 lookup positivo/negativo)
y status_icon_id/status_color_token (mapeo de 4+2 keys). Linkamos solo
helpers — sin ImGui ni vendor extra.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:30 +02:00
egutierrez 4388b54356 feat(viz): render ImGui dod_evidence_panel (issue 0117)
Panel con header (run_id + counts) + tabla 6-col (status icon / id /
kind / expected / evidence preview / actions). Soporta 4 kinds de
evidence: screenshot (stub textual), log (5-line preview + popup),
url (xdg-open/ShellExecuteA) y cmd (diff expected vs actual).
Botones Validate/Reject invocan callbacks on_validate/on_reject.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:23 +02:00
egutierrez b21adb40c9 feat(viz): helpers puros para dod_evidence_panel (issue 0117)
DodItem/DodEvidence/DodPanelState + count_status/find_evidence/
status_icon_id/status_color_token. Sin ImGui — testeable en aislamiento.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:30:16 +02:00
egutierrez 6fd2e9d071 docs: cerrar issue 0115 (worktree launcher Go fn) 2026-05-18 18:24:24 +02:00
egutierrez d9ef4e54f4 docs(capabilities): create 'agents' group + tag audit_dod_schema
New capability group page docs/capabilities/agents.md consolidating:
- agent_launch_worktree_go_infra
- agent_cleanup_worktree_go_infra
- audit_dod_schema_go_infra (added 'agents' tag to its frontmatter)

3 functions = minimum for a capability group page. Adds row to
docs/capabilities/INDEX.md. End-to-end example shows the launch ->
work -> cleanup -> dod-audit cycle that agent_runner_api (0113)
will orchestrate.
2026-05-18 18:24:17 +02:00
egutierrez 2ea9206934 test(infra): agent_launch_worktree + agent_cleanup_worktree
Three tests for launch:
- creates worktree dir + branch off master
- ResetIfExists=true on existing branch+worktree succeeds
- returns Error when required args missing

Two tests for cleanup:
- removes worktree dir and branch after launch
- tolerates missing worktree/branch (cleanup called twice)

Uses initDummyRepo helper with isolated GIT_CONFIG_GLOBAL=/dev/null
so tests do not pick up user's signing/template config. Echo stub
fallback (claude not in PATH) keeps tests hermetic.
2026-05-18 18:24:13 +02:00
egutierrez 355bcac6c7 feat(infra): agent_launch_worktree + agent_cleanup_worktree Go fns
Two Go functions in functions/infra/ for orchestrating headless Claude
agents inside isolated git worktrees:

- AgentLaunchWorktree(cfg): creates worktree off master, spawns
  claude -p in background, redirects stdout/stderr to LogPath. Falls
  back to echo stub when claude binary missing (CI/test friendly).
  ResetIfExists support for re-runs.

- AgentCleanupWorktree(repo, branch, path, pid): SIGTERM with 1s
  grace then SIGKILL, git worktree remove --force, git branch -D.
  Best-effort: only errors when all three steps fail (idempotent
  cleanup-twice).

Promotes inline bash from .claude/skills/parallel-fix-issues/ and
fn-orquestador to first-class registry functions. Closes issue 0115.

Capability group: agents.
2026-05-18 18:24:08 +02:00
egutierrez 4eb4c1cf98 merge issue/0113: agent_runner_api service Go :8486 + agent_runs.db 2026-05-18 18:18:14 +02:00
egutierrez 40aacac590 merge issue/0110: http_request_cpp_core + http_get_json_cpp_core
# Conflicts:
#	cpp/tests/CMakeLists.txt
2026-05-18 18:18:03 +02:00
egutierrez e9bcbecd24 merge issue/0114: DoD evidence schema + fn doctor dod 2026-05-18 18:17:20 +02:00
egutierrez 7eb7b3d0c8 chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:17:08 +02:00
egutierrez 61ec4c8a76 docs: cerrar issue 0110 (http_request + http_get_json cpp/core)
Funciones implementadas, registradas en registry.db via fn index, tests
locales (11 cases, 27 assertions) en verde. Cierra el gap detectado:
patron curl-popen inline duplicado en services_monitor / dag_engine_ui /
data_factory ahora promovido al registry.

Sigue habilitando issue 0111 (apps/process_explorer http_client) cuando
arranque — usara http_request_cpp_core directamente.
2026-05-18 18:15:38 +02:00
egutierrez a843f84a18 docs(cpp/core): registry .md for http_request + http_get_json (issue 0110)
Frontmatter + self-doc sections (Ejemplo, Cuando usarla, Gotchas) following
the contract in .claude/rules/function_growth_and_self_docs.md. Tags include
'http', 'client', 'curl', 'network', 'registry-gap', 'helper' so FTS surfaces
them when an agent asks for HTTP / fetch / GET / Bearer.

http_get_json declares uses_functions: [http_request_cpp_core] so the
dependency is auditable via mcp__registry__fn_uses.
2026-05-18 18:14:56 +02:00
egutierrez 6f3c129a14 test(cpp/core): integration tests for http_request + http_get_json (issue 0110)
Catch2-based tests that fork+exec a local python3 http.server fixture per
test binary. Covers:

  http_request:
    - GET 200 with body
    - GET 404 (HTTP error != transport error)
    - POST with body + Content-Type
    - bearer_token shortcut adds Authorization: Bearer
    - basic_user/basic_pass shortcut adds HTTP Basic (curl --user)
    - invalid URL surfaces transport error (status=0)
    - timeout_ms is honored (bails before server's 3s sleep)

  http_get_json:
    - parses 200 JSON body
    - throws std::runtime_error on 404
    - bearer_token reaches server (verified via echoed Authorization header)
    - throws std::runtime_error on invalid JSON body

Tests skip gracefully if python3 isn't available (server.start() returns
false; SUCCEED with skip message). No external network required.

Local runs (Linux): 21 assertions / 7 cases (http_request), 6 / 4 (get_json),
all passing.
2026-05-18 18:14:48 +02:00
egutierrez bc270db723 docs: cerrar issue 0113 — agent_runner_api scaffold + DoD + worktrees 2026-05-18 18:14:39 +02:00
egutierrez a3a263702b feat(cpp/core): add http_request + http_get_json helpers (issue 0110)
Promotes the inline curl-popen pattern duplicated across apps/services_monitor,
dag_engine_ui, data_factory into two reusable functions in cpp/functions/core/:

- http_request_cpp_core: generic HTTP client (GET/POST/PUT/DELETE/PATCH) via
  cURL CLI through popen. Portable Linux/WSL/MinGW (no link-time libcurl).
  Supports custom headers, raw body, Bearer/Basic auth shortcuts, timeout,
  optional TLS verify skip. Returns status/body/headers/error/duration_ms.

- http_get_json_cpp_core: convenience wrapper over http_request — GET <url>,
  expect 2xx, parse body as nlohmann::json. Throws std::runtime_error on
  transport / non-2xx / parse failure.

Vendors nlohmann/json v3.11.3 single header at cpp/vendor/nlohmann/json.hpp
(MIT). No CMake target needed — header-only; consumers add
cpp/vendor/ to include path.
2026-05-18 18:14:37 +02:00
egutierrez 78c4f593a4 docs: cerrar issue 0114
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:13:14 +02:00
egutierrez 0f72cc8ad3 docs: dod_evidence_schema templates + READMEs (issue 0114)
- docs/templates/issue.md and docs/templates/flow.md include the optional
  dod_evidence_schema: block with realistic example items.
- dev/issues/README.md and dev/flows/README.md document the schema, kinds
  by example, validation rules and the fn doctor dod entrypoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:13:02 +02:00
egutierrez 030e44b027 test(infra): audit_dod_schema covers valid/invalid/malformed/recurse (issue 0114)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:12:54 +02:00
egutierrez ca2e5588cc feat(infra): audit_dod_schema + fn doctor dod (issue 0114)
Adds AuditDodSchema(issuesDir, flowsDir) which scans dev/issues/ and
dev/flows/ frontmatter for the new optional dod_evidence_schema: block.
Validates id uniqueness, kind in {screenshot,log,url,cmd}, expected
non-empty and required bool (default true). Tolerant to malformed YAML
and missing block.

Wires it into fn doctor dod with human-readable caveman output and
--json support.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:12:49 +02:00
egutierrez 5fb2269c00 merge issue/0095-frontend: dag_engine_ui + data_table v1.3 + dev_console + work_tab + frontmatter migration + DoD user-facing 2026-05-17 02:44:39 +02:00
egutierrez 5e6a974a5d feat(dev): issues 0100-0104 — dev_console binary + work_tab + DoD user-facing + frontmatter migration de 146 issues + taxonomia canonica
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:44:04 +02:00
egutierrez 5d2a14e50a docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:07:03 +02:00
egutierrez 212875ed0d chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:33:22 +02:00
egutierrez d6175964e4 data_table v1.3.1: Dots renderer via ImDrawList (font-independent) + recompile all apps with fn_table_viz (issue 0081-O.6)
- Replace TextColored+glyph with ImDrawList::AddCircleFilled in CellRenderer::Dots.
  Dots are now font-independent: no dependency on Unicode glyph coverage. Fixes
  "dots show as ?" on Karla/Roboto/Inter fonts that lack Geometric Shapes block.
- dots_glyph_size now controls circle radius (px) instead of font scale.
- BadgeRule.label is ignored for Dots (documented in data_table_types.h + docs).
- data_table.md bumped to v1.3.1 with capability growth log entry.
- docs/capabilities/data_table_renderers.md: Dots section updated + Common pitfalls
  entry added: "Asumir que cualquier glyph Unicode renderea".
- dag_engine_ui/tabs.cpp: removed stale "● glyph" comment from BadgeRule.
- Recompiled: dag_engine_ui, registry_dashboard, graph_explorer, navegator_dashboard,
  odr_console. All 5 apps deployed to Desktop/apps/. Build Linux + tests 4/4 green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:35:22 +02:00
egutierrez 5974484bd4 data_table v1.3.0: Dots renderer for status timelines + fix dag_engine_ui antipattern + pitfalls doc (issue 0081-O.5)
PARTE A - CellRenderer::Dots (v1.3.0):
- Add Dots=8 to CellRenderer enum (data_table_types.h)
- Add dots_separator/dots_max/dots_show_count/dots_glyph_size fields to ColumnSpec
- Implement draw_cell_custom case Dots in data_table.cpp
  - Parses comma-separated cell value into tokens
  - Looks up each token in badges for color + optional glyph override
  - Per-dot tooltip via tooltip_on_hover
- tql_emit: serialize renderer="dots" + dots_max/dots_show_count/dots_glyph_size/dots_separator
- tql_apply: deserialize all Dots fields
- tql_emit_test: +6 assertions (58 total, 0 failed)
- tql_apply_test: +8 assertions (114 total, 0 failed)
- test_column_specs: +2 tests (10/10 pass)

PARTE B - dag_engine_ui fix: 10 cols -> 6 cols (submodule commit 61314b7)

PARTE C - docs/capabilities/data_table_renderers.md:
- Update to v1.3.0
- Add decision tree for renderer selection
- Add CellRenderer::Dots section with canonical example
- Add Common pitfalls section (multiple columns, badge for free-text, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:24:53 +02:00
egutierrez 67fff0d677 docs(dag_engine): README autoritativo (anadir DAGs + formato YAML + troubleshooting)
apps/dag_engine/README.md cubre:
- Donde viven los DAGs y como apuntar el systemd unit.
- Workflow paso a paso para anadir uno nuevo (crear/validar/probar/recargar/verificar).
- Formato YAML completo: top-level fields + step fields + cron schedule + ejemplo de extremo a extremo (env, depends, retry_policy, continue_on, handlers).
- Comandos CLI (run/list/status/validate/server) + flags.
- 7 secciones de "que hacer si algo falla": DAG invisible, validation fail, step fallido, scheduler no dispara, WS disconnected, cleanup runs viejos, restaurar backup.
- Endpoints HTTP completos.
- Referencias a funciones del registry y commit de migracion.

app.md de dag_engine + dag_engine_ui apuntan a README.md.
gitlink dag_engine_ui actualizado a commit con app.md mejorado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:23:33 +02:00
egutierrez 890f641692 feat(dag_engine_ui): gitlink panel Timeline (ImPlot scatter X=tiempo Y=DAG)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:14:16 +02:00
egutierrez ae5d27a5ec feat(dag_engine): /api/dags devuelve last_runs[] (max 5) + gitlink UI badges
- executor.go: DagInfo anade LastRuns []store.DagRun. Pobla con e.store.ListRuns(name, 5, 0).
- cpp/apps/dag_engine_ui: gitlink al SHA con 5 puntitos R1..R5 via data_table BadgeRule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:07:16 +02:00
egutierrez 0ed949d235 fix(dag_engine_ui): gitlink Win32 ws2_32 link (issue 0095)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:01:06 +02:00
egutierrez c438dc6916 data_table: Phase 2 — Button + events + tooltip + RightClick + TQL persist column_specs (issue 0081-O)
- CellRenderer::Button=5: renders SmallButton per cell; emits TableEvent::ButtonClick on click
- TableEventKind enum (ButtonClick/RowDoubleClick/RowRightClick/CellEdit) + TableEvent struct
- render() extended overload: adds events_out parameter (nullptr = back-compat, no events)
- RowDoubleClick and RowRightClick detection in raw table loop (stage 0)
- RowRightClick also detected in aggregated stage table (stage 1+)
- Tooltip per cell: tooltip_on_hover + tooltip fields on ColumnSpec; "auto" = show cell value
- State::aux_column_specs: TQL-persisted column specs sidecar per table
- tql_emit: serializes aux_column_specs[0] as column_specs block (badge/progress/duration/icon/button/tooltip)
- tql_apply: parses column_specs block back into state.aux_column_specs[0]
- render() merges aux_column_specs into TableInput when caller passes empty column_specs
- test_column_specs: 5->8 tests (Button struct, tooltip fields, both render() signatures link)
- tql_emit_test: 3 new tests (column_specs badge/button/tooltip emit) — 52 passed
- tql_apply_test: 3 new tests (column_specs badge/button/tooltip roundtrip) — 106 passed
- Back-compat: existing apps (graph_explorer, registry_dashboard) unchanged
- Version bump: data_table v1.1.0 -> v1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:59:26 +02:00
egutierrez 4027aeaaf5 feat(dag_engine_ui): gitlink tabs DAG List/Detail/Run Detail (issue 0095 step 5)
cpp/apps/dag_engine_ui: SHA con data_table_cpp_viz integrado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:57:59 +02:00
egutierrez 9ff0b3900c feat(dag_engine_ui): gitlink WS client (issue 0095 step 4)
cpp/apps/dag_engine_ui: SHA con ws_client + panel Live integrado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:47:23 +02:00
egutierrez ce9fa3b451 feat(dag_engine): json tags lowercase + gitlink dag_engine_ui HTTP layer (issue 0095 step 3)
- store/store.go: anade tags JSON lowercase a DagRun + DagStepResult para que REST y WS devuelvan misma forma.
- cpp/apps/dag_engine_ui: gitlink al SHA con http_client + data_http.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:45:18 +02:00
egutierrez e0cce972ea feat(cpp): registrar dag_engine_ui en cpp/CMakeLists.txt (issue 0095 step 2)
Sub-repo Gitea: dataforge/dag_engine_ui (a crear cuando se ejecute /full-git-push).
Gitlink al SHA inicial del scaffolding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:38:43 +02:00
egutierrez 380a7a8f35 data_table: declarative cell renderers Phase 1 (Badge/Progress/Duration/Icon)
Adds TableInput.column_specs sidecar field enabling apps to declare Badge,
Progress, Duration and Icon renderers per column without writing ImGui inline.
Back-compat: apps without column_specs compile and behave identically.

- data_table_types.h: CellRenderer enum, BadgeRule, IconMapEntry, ColumnSpec types
- data_table.cpp: hex_to_imcolor helper, icon_name_to_glyph static map (~30 Tabler icons),
  draw_cell_custom dispatcher, integration in Stage-0 and Stage-N cell loops and draw_extra_panel
- Bump version 1.0.0 -> 1.1.0 with capability growth log
- cpp/tests/test_column_specs.cpp: 5 smoke/linker tests (back-compat + 4 renderer types)
- cpp/tests/CMakeLists.txt: register test_column_specs target linked against fn_table_viz
- types/core/{cell_renderer,badge_rule,icon_map_entry,column_spec}.md: registry type mds
- docs/capabilities/data_table_renderers.md: canonical doc with end-to-end examples
- docs/capabilities/INDEX.md: added data-table-renderers group

All tests green: test_column_specs 5/5, test_fn_table_viz_smoke 8/8,
tql_emit 41/41, tql_apply 88/88, Wave-1 tests 8/8.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:38:24 +02:00
egutierrez 7ecbee1175 feat(dag_engine): WS hub /api/ws/dagruns + migracion DAGs desde dagu
- events.go: DagRunHub broadcastea snapshot+deltas live (500ms tick, 5s recent finished window) sobre dag_runs + dag_step_results.
- api.go: handler GET /api/ws/dagruns upgrade WS, opt-in en RegisterAPI.
- store.go: expone Conn() para read-only desde el hub.
- main.go: construye DagRunHub al arrancar server.
- dags_migrated/: 5 YAMLs migrados desde ~/dagu/dags tras desinstalar dagu (issue 0095 step 1).

Smoke: snapshot inicial OK, trigger /api/dags/test_claude_access/run -> delta WS observa 3 step_results + run success en <1s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:36:34 +02:00
egutierrez fe39de8b22 scaffolder: auto-wire fn_table_viz in new C++ apps (issue 0081-M)
- CMakeLists.txt template: adds target_link_libraries fn_table_viz with
  if(TARGET fn_table_viz) guard (compiles even without vendor/lua).
- main.cpp template: adds commented include + data_table::render() panel
  block in render(). Also fixes include to use app_base.h + panel_menu.h
  (matching convention of chart_demo, shaders_lab).
- app.md template: uses_functions lists all 12 fn_table_viz stack IDs
  commented out; uncomment when activating data_table::render().
- Bump version 0.1.0 -> 1.1.0. Add capability growth log entry.
- Tag cpp-tables added to capability group.
- Verified: test app test_scaffolder_default compiled clean, residues
  removed (dir + CMakeLists entry).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:06:06 +02:00
egutierrez 951a77ec7f close issue 0081: tables promoted to registry + fn doctor cpp-apps BeginTable check
- docs/TQL.md: añadidas secciones joins, views, main_source, 24 viz tokens completos
  (extraidos de tql_helpers.cpp), color_rules, fn.* builtins completos (20 funciones),
  funciones bloqueadas del sandbox, tabla de estado de implementacion actualizada.
  Nota al pie referencia los 129 checks roundtrip (41 emit + 88 apply).

- functions/infra/audit_cpp_apps.go: añadida AuditCppTableMigration() que escanea
  .cpp de cada app imgui buscando ImGui::BeginTable; status CANDIDATE/MIXED/clean
  segun si usa data_table_cpp_viz en uses_functions.

- cmd/fn/doctor.go: fn doctor cpp-apps ahora incluye seccion BeginTable migration
  con tabwriter CANDIDATE/MIXED; --json produce {conformance, table_migration}.
  doctorAll incluye cpp_table_migration en el mapa JSON.

- .claude/rules/fn_doctor.md: tabla de subcomandos y acciones complementarias
  actualizadas con el nuevo check.

- dev/issues/0081 movido a completed/ con status done y notas de deuda documentadas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:49:56 +02:00
672 changed files with 77383 additions and 20272 deletions
+4 -1
View File
@@ -21,12 +21,14 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps/<name>/` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). Ver `.claude/rules/apps_subrepo.md`.
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
**Reglas y convenciones:** ver `.claude/rules/INDEX.md`
**Slash commands:** `/commands` lista todos los slash commands del repo agrupados por namespace (global + projects). Project commands viven en `projects/<p>/.claude/commands/` y se exponen como `/<project>:<cmd>` via symlink. Ver `.claude/rules/project_commands.md`.
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
---
@@ -258,6 +260,7 @@ fn check params # Lista funciones sin params_schema
fn doctor # Corre todos los checks
fn doctor artefacts # git/venv/app.md/upstream de cada app y analysis
fn doctor services # apps tag 'service' + systemctl + puerto
fn doctor services-spec # audita bloque `service:` del app.md (issue 0105)
fn doctor sync # drift pc_locations BD vs disco
fn doctor uses-functions # imports reales vs uses_functions del app.md
fn doctor unused # funciones del registry sin consumidores
+10
View File
@@ -30,6 +30,16 @@ Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks
6. **Auditoria total**. Cada decision se loggea en `task_runs.progress_json` con razonamiento + fase + run_id.
7. **No self-modify**. NO modificas tu propio SKILL.md ni el de otros subagentes en la misma run.
8. **Cero produccion**. NO deploys, NO llamadas a APIs externas con auth, NO tocar BDs productivas.
9. **NUNCA paths absolutos fuera del worktree**. SIEMPRE rutas relativas o absolutas que apunten dentro de `/tmp/fn_orq_<issue>_<ts>/`. Si necesitas leer algo del repo principal (ej. plantillas docs), copialo al worktree primero. Refuerzo del piloto 1 (2026-05-15): orquestador modifico hooks bash del repo principal usando paths absolutos `/home/lucas/fn_registry/bash/functions/...` para destrancar pre-commit. Solucion correcta: el fix vive en el worktree, NO en main.
10. **Pre-commit hook compartido**. Worktrees comparten `.git/hooks/` con main repo. Si el hook llama scripts via path absoluto a main (ej. `/home/lucas/fn_registry/bash/functions/cybersecurity/scan_secrets_in_dirty.sh`), el hook ejecutara la version de MAIN, no la del worktree. Opciones legitimas:
a. Aplicar el fix del hook EN EL WORKTREE y commitearlo en `auto/*` — al mergear el PR, main heredara el fix.
b. Si el hook bloquea progreso y el fix del hook excede tu scope, `git commit --no-verify` para ESE commit SOLO, documentando excepcion en `task_runs.events_json[].decision="skip_hook"` con razon.
NO modificar archivos en main directamente.
11. **Post-iteracion sanity check**. Tras cada commit en `auto/*`, verificar:
```bash
git -C /home/lucas/fn_registry status --short
```
Si la salida cambia respecto al baseline (capturado al inicio del piloto), HAS contaminado el repo principal. ABORT con `status=sandbox_breach` y reporta los archivos afectados en el output al humano.
---
+1
View File
@@ -0,0 +1 @@
../../projects/aurgi/.claude/commands
+23 -108
View File
@@ -1,121 +1,36 @@
# /autonomous-task — Lanza fn-orquestador (Fase 6 del ciclo reactivo)
Lanza el meta-orquestador autonomo que recorre el bucle CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR sobre un issue, sin intervencion humana, hasta convergencia / estancamiento / timeout / limite de iteraciones.
Issue 0069. Pre-condiciones obligatorias (chequear ANTES de despachar):
1. Migration `fn_operations/migrations/006_task_runs.sql` aplicada.
2. Subagentes `fn-constructor`, `fn-executor`, `fn-recopilador`, `fn-analizador`, `fn-mejorador`, `fn-orquestador` presentes en `.claude/agents/`.
3. `dev/autonomous_protected_paths.json` existe.
4. `master` local up-to-date con `origin/master`.
5. Branch `auto/<issue_id>` NO existe ya.
6. `gh auth status` OK (necesario para PR draft al converger).
7. Tipo de tarea soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`.
Si alguna pre-condicion falla → ABORT con razon. NO improvisar.
---
description: "DEPRECADO 2026-05-19 — usa /autopilot. Wrapper directo a fn-orquestador conservado solo como debug primitive."
---
## Argumento
# /autonomous-task — DEPRECADO (sustituido por `/autopilot`)
`$ARGUMENTS``<issue_id>` o `<task_spec_path>` + flags opcionales.
**ESTADO:** deprecado 2026-05-19. Usa `/autopilot <NNNN>` en su lugar.
```
/autonomous-task 0070
/autonomous-task 0070 --max-iterations 15 --max-minutes 90
/autonomous-task 0070 --auto-apply-proposals safe
/autonomous-task 0070 --dry-run
/autonomous-task path/to/spec.yaml --branch auto/custom-name
```
## Por que deprecado
Flags:
- `--max-iterations N` tope de iteraciones (default 10)
- `--max-minutes M` timeout total (default 60)
- `--auto-apply-proposals` `none|safe|aggressive` (default `safe`)
- `--branch NAME` rama TBD (default `auto/<issue_id>`)
- `--dry-run` simula, NO aplica
`/autopilot` (v2, 2026-05-19) absorbe la funcionalidad y anade:
- Pre-flight DoD readiness check (gate STOP — no arranca sin DoD).
- Detector issue vs flow.
- Reporte estructurado al humano post-delegate.
- Self-Q&A migrado a fn-orquestador.
---
Behaviour orquestador-side es identico. La unica diferencia es que `/autopilot` valida antes de delegar; `/autonomous-task` delegaba ciego.
## Comportamiento
## Sustitucion 1:1
1. **Verificar pre-condiciones** con script bash (ver arriba). Si alguna falla, reportar y salir.
2. **Despachar a `fn-orquestador`** via Agent tool con `subagent_type=fn-orquestador`. Pasar:
- `issue_id` o `task_spec`
- flags resueltos
- paths protegidos (leidos de `dev/autonomous_protected_paths.json`)
3. **El subagente:**
- Crea worktree aislado `/tmp/fn_orq_<issue>_<ts>/` desde `master`.
- Persiste estado en `task_runs` (operations.db del app target o repo root).
- Despacha por fases a los 5 subagentes especializados.
- Aplica proposals filtradas por `--auto-apply-proposals`.
- Termina con: `converged` (PR draft creado) | `stalled` | `timeout` | `iterations_exhausted` | `needs_human` | `aborted`.
4. **Reportar resultado al humano** con:
- `status`, `iterations / max`, `duration / max`
- `branch`, `worktree`, `PR draft url` si converged
- `proposals creadas / aplicadas`
- `last run_id` y status
- Resumen iter-por-iter del `progress_json`
| Antes | Ahora |
|---|---|
| `/autonomous-task 0070` | `/autopilot 0070` |
| `/autonomous-task 0070 --max-iterations 15 --max-minutes 90` | `/autopilot 0070 --max-iterations 15 --max-minutes 90` |
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
| `/autonomous-task 0070 --auto-apply-proposals safe` | `/autopilot 0070 --auto-apply-proposals safe` |
---
## Modo debug
## Reglas duras (no negociables)
Si `/autopilot` falla en pre-flight pero quieres forzar dispatch sin DoD check (debug / experimentos), puedes seguir usando `/autonomous-task` que va directo a `fn-orquestador` sin validar. NO RECOMENDADO para uso normal.
- Sandbox de rama EN WORKTREE — nunca toca master ni el working tree del humano.
- No merge automatico — PR draft siempre.
- No `--no-verify`, no `--force`, no skip hooks.
- Paths protegidos via `dev/autonomous_protected_paths.json`.
- Watchdog: 2 iteraciones con mismo set de fails → `status=stalled`.
- Auditoria total en `task_runs.progress_json`.
- No self-modification: NO toca `.claude/agents/` ni `.claude/commands/`.
## Migration deadline
---
Sin deadline duro — `/autonomous-task` seguira funcionando hasta que un commit lo elimine. Pero NO se anaden nuevas features aqui; cualquier mejora va a `/autopilot`.
## Integracion con call_monitor (issue 0085)
El orquestador puede leer `projects/fn_monitoring/apps/call_monitor/operations.db` para:
- Consultar `function_stats` antes de decidir que funciones usar/reusar.
- Filtrar proposals existentes via `mcp__registry__fn_proposal --status pending` para evitar duplicados.
- Loggear sus invocaciones via el hook PostToolUse (automatico).
Tras converger, el `call_monitor propose` ejecutado por el humano (o futuro cron) absorbera las nuevas violations / copied_code / fails para alimentar la siguiente ronda.
---
## Tipos NO soportados
- Diseño arquitectura nuevo (humano decide).
- Decisiones UX subjetivas.
- Cambios BD productiva.
- Cualquier cosa que toque secrets/credenciales.
- Self-modification del propio orquestador.
Si el issue contiene criterios no-verificables programaticamente, ABORT con `status=needs_human`.
---
## Output canonico
```
=== /autonomous-task: 0070 ===
status: converged
iterations: 7 / 10
duration: 23 min / 60
branch: auto/0070
worktree: /tmp/fn_orq_0070_1731612345
PR draft: https://github.com/.../pull/123
proposals: 3 creadas, 2 auto-aplicadas
last run_id: e2e_run_abc123 (status: pass)
Iter:
1. construir → ok (2 funciones nuevas)
2. ejecutar → ok
3. analizar → fail (2/8 checks)
4. mejorar → 3 proposals (2 auto-applicadas)
5. construir → ok (re-build tras patches)
6. analizar → pass
7. recopilador → ok (operations.db integra)
Siguiente: revisar PR draft + fn proposal list -s pending --target-id 0070
```
Ver `.claude/commands/autopilot.md` para spec completa.
+212
View File
@@ -0,0 +1,212 @@
---
name: autopilot
description: Modo full-auto. Pre-flight DoD check, detecta issue vs flow, SIEMPRE delega a fn-orquestador (worktree aislado + PR Gitea). Sin Path inline. Sustituye a /autonomous-task.
---
# /autopilot — Comando autonomo unificado
Comando UNICO para ejecutar issue o flow autonomo end-to-end. Sustituye a `/autonomous-task` (deprecado). Hace dos cosas:
1. **Pre-flight DoD readiness check** — sin DoD claro, no arranca.
2. **Delega SIEMPRE a `fn-orquestador`** via Agent tool — worktree aislado en `/tmp/fn_orq_<NNNN>_<ts>/`, branch `auto/<NNNN>-<slug>`, PR draft Gitea al converger.
NO ejecuta nada inline. NO muta cwd del shell del humano. NO duplica worktrees. Toda la complejidad de bucle + paths protegidos + sanity check vive en `fn-orquestador`.
## Por que solo delegar
Historico: versiones anteriores de `/autopilot` tenian Path A (delegate a orquestador), Path B (registry-only inline), Path C (flow inline). Los Path B/C reimplementaban lo que ya hace `fn-orquestador` (worktree, branch, PR) y arrastraban un bug: `cd` en Bash de Claude Code PERSISTE entre llamadas → si autopilot hace `cd "$WT"`, todos los Bash subsiguientes operan en branch incorrecta. Solucion: NO hacer Path inline, delegar siempre.
`fn-orquestador` ahora soporta dos `task_type`:
- `issue` — flujo CONSTRUIR→EJECUTAR→RECOPILAR→ANALIZAR→MEJORAR (default).
- `flow` — parsea `dev/flows/<NNNN>-*.md` ## Flow y ejecuta steps (Path C absorbido).
## Sintaxis
```
/autopilot <NNNN> # issue NNNN (default si no hay prefijo)
/autopilot issue:<NNNN> # issue explicito
/autopilot i:<NNNN> # alias
/autopilot flow:<NNNN> # flow NNNN
/autopilot f:<NNNN> # alias
/autopilot check <target> # solo audita DoD readiness, no delega
/autopilot <target> --max-iterations N --max-minutes M --dry-run
```
Detector:
- `^\d{4}[a-z]?$` → issue (sin prefijo = issue por defecto).
- `^(issue|i):\d{4}[a-z]?$` → issue.
- `^(flow|f):\d{4}$` → flow.
- Otra cosa → ABORT con error de sintaxis.
## Pre-flight DoD readiness check (OBLIGATORIO)
Sin DoD claro, autopilot no delega. Verificacion es STOP-gate.
### Issue (`dev/issues/<NNNN>-*.md`)
1. Archivo existe en `dev/issues/` (no en `completed/`).
2. Frontmatter con `status`, `priority`.
3. Al menos UNA de:
- `## DoD` o `## Definition of Done` con >=1 bullet/checkbox concreto.
- `## Acceptance` con checkboxes `[ ]`.
- `## Tests` + `## Tareas` ambas no vacias.
4. Tipo declarado/inferible soportado por `fn-orquestador`: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`, `feature_registry_only`.
5. NO contiene criterios no-verificables ("queda bonito", "intuitivo", "UX mejor"). Grep simple; si match → ABORT con warning.
### Flow (`dev/flows/<NNNN>-*.md`)
1. Archivo existe en `dev/flows/`.
2. Frontmatter valido.
3. `## Acceptance` con >=1 checkbox.
4. `## Flow` no vacio.
5. Pre-requisitos declarados.
6. Tabla de funciones recomendadas sin `FALTA: crear <id>` (si los hay → ABORT salvo `--allow-construct-missing`).
Si falla:
```
=== /autopilot check 0125 ===
status: NOT READY
target: issue 0125 (skill-tree-dashboard-panel)
gaps:
- Sin seccion DoD/Acceptance
- "UX intuitiva" linea 47 — no verificable
fix:
- Anadir ## DoD con 3-5 bullets programaticamente verificables
- Reemplazar criterios subjetivos por mediciones concretas
```
Si OK:
```
=== /autopilot check 0107c ===
status: READY
target: issue 0107c (refactor data_table)
dod_items: 5 checkboxes
task_type: refactor_safe
estimated_iter: 3-5
```
## Dispatch a fn-orquestador
Tras pre-flight OK, ejecuta:
```
Agent(
subagent_type="fn-orquestador",
prompt="""
Issue/Flow: <path al .md>
Modo: REAL (o --dry-run)
task_type: <issue|flow>
Pre-condiciones verificadas: 7/7 verde
Master: <sha> sync con origin
Working tree principal: limpio (baseline)
Max iter: N
Max min: M
Auto-apply proposals: safe
Token Gitea: pass gitea/dataforge-git-token
DB task_runs: apps/deploy_server/operations.db (schema task_id)
Reglas duras: autonomous_loop.md (11 reglas)
""",
run_in_background=true
)
```
Cuando termine, reporta al humano con output canonico del orquestador:
```
=== /autopilot 0121b ===
target: issue 0121b (fn doctor e2e-coverage)
delegated_to: fn-orquestador
status: converged
iterations: 1 / 8
duration: 4 min / 30
task_run_id: task_d285372493cce2e6
branch: auto/0121b-orquestador
worktree: /tmp/fn_orq_0121b_1779147778
PR draft: https://gitea-.../dataforge/fn_registry/pulls/3
Siguiente: revisar PR, mergear, mover issue a completed/
```
## Reglas duras (autopilot-level)
1. **Cero cwd mutation**. Autopilot NUNCA hace `cd`. Usa `git -C <repo>` siempre si necesita inspeccionar.
2. **Cero ejecucion inline de bucle**. Todo va via `fn-orquestador`. Si autopilot necesita ejecutar algo (pre-flight scripts), es read-only.
3. **Cero AskUserQuestion**. Self-pick "Recommended". Si no hay, ABORT con `status=needs_human`.
4. **DoD es contrato**. Si DoD no se cumple al final, `task_run.status` queda `partial` y autopilot reporta NOT_DONE — humano decide.
5. **Worktree gestion delegada al orquestador**. Autopilot NO crea worktrees propios. NO toca branches.
6. **Trazabilidad**: cada decision pre-delegate (especialmente abort de DoD check) se persiste en `task_runs.events_json[]` con `agent: autopilot`.
## Flags
| Flag | Default | Que hace |
|---|---|---|
| `--max-iterations N` | 10 | Pasado al orquestador |
| `--max-minutes M` | 60 | Pasado al orquestador |
| `--dry-run` | off | Pasado al orquestador |
| `--allow-construct-missing` | off | Flow con `FALTA: crear <id>` → spawn fn-constructor antes |
| `--auto-apply-proposals` | `safe` | Pasado al orquestador |
## Errores canonicos
| Codigo | Significado | Accion |
|---|---|---|
| `NOT_READY` | DoD insuficiente | Humano edita .md y relanza |
| `needs_human` | Decision ambigua | Humano resuelve y relanza |
| `delegated_failed` | fn-orquestador devolvio fail/stall/timeout | Humano lee `task_runs.events_json` |
| (resto) | Heredados del orquestador (stalled/timeout/aborted_protected_path/...) | Idem |
## Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Hacer Path B/C inline | Mismo bug de cwd mutation que paso 2026-05-19 |
| Saltar pre-flight DoD | Trabajar sin contrato = bucle infinito |
| Mergear sin tests verde | fn-orquestador ya impide esto, NO bypaseas |
| `AskUserQuestion` desde autopilot | Rompe contrato autonomo |
| Crear worktree propio en autopilot | Duplica + colision con orquestador (paso 2026-05-19) |
## Ejemplos
```bash
# Issue con DoD claro
/autopilot 0107c
# Flow con piezas faltantes — autoriza creacion antes
/autopilot flow:0008 --allow-construct-missing
# Solo audit
/autopilot check 0125
/autopilot check flow:0008
# Dry run
/autopilot 0107c --dry-run
```
## Relacion con otras reglas
- [[autonomous_loop]] — politica del bucle (sandbox, paths protegidos, watchdog). fn-orquestador la aplica.
- [[apps_tbd]] — politica TBD por tipo de cambio.
- [[apps_subrepo]] — `git init` dentro de apps nuevas antes de limpiar worktree.
- [[feature_flags]] — codigo incompleto detras de flag OFF.
- [[registry_calls]] — invocaciones canonicas.
- [[e2e_validation]] — `e2e_checks` consumidos por fn-analizador.
- [[delegation]] — spawn fn-constructor antes que escribir inline.
## Migracion desde `/autonomous-task`
`/autonomous-task` queda DEPRECADO. Sustitucion 1:1:
| Antes | Ahora |
|---|---|
| `/autonomous-task 0070` | `/autopilot 0070` |
| `/autonomous-task 0070 --max-iterations 15` | `/autopilot 0070 --max-iterations 15` |
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
`/autopilot` anade pre-flight DoD check + detect flow. Behaviour orquestador-side idem.
## Historico
- v1 (2026-05-15): introducido con Path A/B/C inline + self-Q&A.
- v2 (2026-05-19): simplificado tras incidente cwd mutation en piloto 0121b. Solo delega a fn-orquestador. Self-Q&A movido al orquestador. Sustituye a `/autonomous-task`.
+86
View File
@@ -0,0 +1,86 @@
---
description: "Lista todos los slash commands disponibles en el repo: globales de fn_registry + namespaced de cada project. Filtra por substring o por namespace."
---
# /commands — Catalogo de slash commands del repo
Inventario unificado. Lista los `.md` bajo `.claude/commands/` (recursivo, sigue symlinks) y agrupa por namespace.
## Sintaxis
```
/commands # listado completo agrupado por namespace
/commands <substring> # filtra por substring en nombre o descripcion
/commands --ns <namespace> # solo un namespace (global, aurgi, ...)
/commands --json # salida JSON para agentes
```
## Implementacion
Bash + awk. Parsea frontmatter `description:` de cada `.md`. Agrupa por subdirectorio (subdir = namespace, root = `global`).
```bash
#!/usr/bin/env bash
set -euo pipefail
ROOT="${FN_REGISTRY_ROOT:-/home/egutierrez/fn_registry}"
CMD_DIR="$ROOT/.claude/commands"
# Recolecta: ns|name|description
collect() {
find -L "$CMD_DIR" -type f -name '*.md' | while read -r f; do
rel="${f#$CMD_DIR/}"
case "$rel" in
*/*) ns="${rel%%/*}"; name="${rel#*/}"; name="${name%.md}" ;;
*) ns="global"; name="${rel%.md}" ;;
esac
desc=$(awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit}' "$f")
printf '%s|%s|%s\n' "$ns" "$name" "${desc:-(sin descripcion)}"
done | sort
}
collect | awk -F'|' '
{
if ($1 != prev_ns) {
if (prev_ns) print ""
if ($1 == "global") print "## global (/<cmd>)"
else print "## " $1 " (/" $1 ":<cmd>)"
prev_ns = $1
}
printf "- /%s%s — %s\n", ($1=="global"?"":$1":"), $2, $3
}'
```
Filtros:
- Substring: `grep -i "<substring>"` sobre stdout.
- `--ns X`: filtrar antes del `awk` por `$1 == "X"`.
- `--json`: reemplazar el `awk` por `jq -Rsn` que construya array `{namespace, name, description, invocation}`.
## Salida (formato humano)
```
## global (/<cmd>)
- /app — Crear, configurar y desplegar apps del registry
- /autopilot — Modo full-auto...
- /commands — Catalogo de slash commands del repo
...
## aurgi (/aurgi:<cmd>)
- /aurgi:anadir_contexto_aurgi — Anade o modifica contexto...
- /aurgi:aumentar_task — Enriquece tarea Aurgi con preguntas...
- /aurgi:contexto_aurgi — Aprende el contexto de Aurgi...
```
## Cuando usarlo
- Sesion nueva: ver de un vistazo que slash commands hay disponibles.
- Antes de inventar logica inline: comprobar si ya existe un command.
- Auditoria: verificar que los projects exponen sus commands correctamente.
- Onboarding: nuevo PC clonado, descubrir capacidades del repo sin abrir N archivos.
## Gotchas
- Sigue symlinks (`find -L`). Si un symlink apunta a directorio inexistente, devuelve vacio para esa rama — verificar con `ls -L .claude/commands/<ns>/`.
- Solo escanea `<root>/.claude/commands/`. Commands user-global en `~/.claude/commands/` NO entran (son personales, fuera del repo).
- Namespace = nombre del subdirectorio bajo `.claude/commands/`. Coincide con el project pero no por mecanismo — por convencion. Ver `.claude/rules/project_commands.md`.
- Para que un command de project aparezca aqui desde la raiz, hace falta el symlink (`.claude/commands/<project>` -> `../../projects/<project>/.claude/commands`).
+274
View File
@@ -0,0 +1,274 @@
# /cpp-app — Crear o modificar app C++ del registry sin olvidar nada
Recopila TODOS los datos necesarios (frontmatter, trio app_hub, panels, AppConfig, service block, e2e_checks, uses_functions) **antes** de tocar el disco. Tras confirmar, ejecuta scaffolder o edits, regenera iconos, refresca app_hub, compila y deploya a Windows.
Sustituye al flujo manual "edito main.cpp + app.md + CMakeLists.txt a mano". Wrapper sobre `init_cpp_app_bash_pipelines` (create) o edits directos sobre `app.md` (modify) + `regenerate_app_icons` + `refresh_app_hub` + `redeploy_cpp_app_windows`.
---
## Uso
```
/cpp-app # interactivo, modo create
/cpp-app <name> # interactivo, modo create con name pre-rellenado
/cpp-app modify <name> # editar app existente
```
---
## Modo CREATE — flujo turno a turno
Si `$ARGUMENTS` no empieza por `modify`, es create. Si trae `<name>`, lo usas como default; si no, pregunta name.
### Paso 0 — verificar que no existe
```bash
test -d "/home/lucas/fn_registry/apps/<name>" \
|| ls /home/lucas/fn_registry/projects/*/apps/<name> 2>/dev/null
```
Si existe en cualquier ubicacion: **abortar** y sugerir `/cpp-app modify <name>`. NO sobreescribir.
### Paso 1 — Identidad (AskUserQuestion)
1. **name** (texto libre — valida snake_case + contiene verbo segun `ids_naming.md`). Verbos canonicos: `show, render, view, plot, edit, manage, monitor, browse, explore, run, launch, scan, audit, debug, profile, ...`. Si no trae verbo, sugerir alternativas (`viewer` -> `<name>_viewer`).
2. **project** (select: ninguno / lista de `projects/*/`). Si ninguno -> `apps/<name>/`.
3. **domain** (select: `tools` (default), `gfx`, `tui`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `pipelines`, `browser`).
4. **description** 1 linea (texto libre, max 80 chars). **OBLIGATORIO** — sin esto el hub muestra tarjeta vacia.
### Paso 2 — Trio app_hub OBLIGATORIO
Regla dura `cpp_apps.md`: description + icon.phosphor + icon.accent SIEMPRE juntos.
5. **icon.phosphor** glyph name. Antes de preguntar, ofrece busqueda:
```bash
ls /home/lucas/fn_registry/sources/phosphor-core/assets/fill/ | grep -i "<keyword>"
```
Sugiere 3-5 candidatos basados en `description`. Default segun domain: `gfx`->`palette`, `tui`->`terminal`, `tools`->`wrench`, `infra`->`gear`, `finance`->`chart-line-up`, `datascience`->`graph`, `cybersecurity`->`shield`.
6. **icon.accent** hex `#rrggbb` (palette select):
- sky `#0ea5e9`, indigo `#4f46e5`, violet `#7c3aed`, pink `#ec4899`, rose `#f43f5e`, red `#dc2626`, orange `#ea580c`, amber `#d97706`, green `#16a34a`, teal `#0d9488`, cyan `#0891b2`, slate `#475569`. Default segun domain.
### Paso 3 — Tags
7. **tags** (multiSelect): `service`, `launcher`, `dashboard`, `viewer`, `editor`, `monitor`, `debug`, `prototype`. Si selecciona `service` -> activar bloque service (Paso 7).
### Paso 4 — Panels iniciales
8. **panels** (texto libre o select):
- Default: 1 panel `Main` (Ctrl+1).
- Opcion lista: hasta 4 paneles. Por cada uno: `{label, shortcut}`. Generara `PanelToggle k_panels[]` en `main.cpp`.
### Paso 5 — AppConfig flags
9. (multiSelect):
- `init_gl_loader` (true si la app llama `gl*` directo, ej. shaders, GPU renderer custom). Default false.
- `viewports` true (default) / false (single-window).
- `auto_dockspace` true (default) / false (solo si gestiona DockSpace propio tipo `shaders_lab`).
- `fps_overlay` activo de inicio? (controla solo el default; el menu Settings lo toggle).
### Paso 6 — Funciones del registry a usar
10. **uses_functions** lista IDs. Antes de preguntar, busca candidatas segun description:
```
mcp__registry__fn_search query="<keyword>" entity="functions"
```
Y muestra capability groups relevantes (`docs/capabilities/INDEX.md`). El usuario puede aceptar lista, anadir IDs, o dejar vacio (se rellena tras codear).
Cada ID que no este en el registry -> ofrecer spawn `fn-constructor` antes de continuar (regla `delegation.md`).
### Paso 7 — Bloque `service:` (solo si tag=service)
11. Si paso 3 marco `service`, recopilar (regla `function_tags.md` + issue 0105):
- `port` int o null
- `health_endpoint` ruta GET o null
- `health_timeout_s` (default 3)
- `runtime` (select: `systemd-user`, `systemd-system`, `docker-compose`, `stdio`, `manual`)
- `systemd_unit` (obligatorio si runtime empieza por `systemd-`)
- `systemd_scope` (`user|system|null`)
- `restart_policy` (select: `always` (Recommended — gotcha: `on-failure` NO reinicia SIGTERM limpio), `on-failure`, `none`)
- `pc_targets` (multiSelect de pc_locations actuales: `aurgi-pc`, `home-wsl`, ...)
- `is_local_only` (true/false default false)
### Paso 8 — Persistencia
12. (multiSelect):
- BD propia SQLite `<name>.db` en `local_files/`? -> recordar usar `fn::local_path("<name>.db")` (cpp_apps.md §7)
- operations.db (para entities/relations)? -> ejecutar `fn ops init` tras crear
- Archivos config en `local_files/`?
### Paso 9 — e2e_checks (issue 0068)
13. Default sugerido (modificable):
```yaml
e2e_checks:
- id: build
cmd: "cmake --build cpp/build --target <name> -j"
timeout_s: 300
- id: self_test
cmd: "./cpp/build/apps/<name>/<name> --self-test"
timeout_s: 30
severity: warning # si todavia no implementa --self-test
```
Pregunta: ¿anadir mas checks (ops_audit, pytest, smoke)?
### Paso 10 — Resumen y confirmacion
Mostrar bloque YAML completo del `app.md` que se va a generar + flags del scaffolder + post-acciones. Pedir confirmacion antes de ejecutar.
---
## Modo CREATE — ejecucion
Una vez confirmado:
```bash
cd /home/lucas/fn_registry
# 1. Scaffolder
./fn run init_cpp_app <name> \
[--project <p>] \
[--domain <d>] \
--desc "<description>" \
[--tags "<csv>"]
# 2. Editar app.md generado para anadir:
# - icon: {phosphor, accent}
# - service: {...} (si aplica)
# - uses_functions: [...]
# - e2e_checks: [...]
# (el scaffolder no rellena estos; editarlos con Edit tool)
# 3. Editar main.cpp generado para reflejar:
# - panels[] custom (si != default)
# - cfg.init_gl_loader / cfg.auto_dockspace / cfg.viewports
# - includes de funciones registry usadas
# 4. Editar CMakeLists.txt para anadir paths de funciones del registry:
# ${CMAKE_SOURCE_DIR}/functions/<d>/<f>.cpp
# 5. Si es service -> ofrecer crear systemd unit (skipear si runtime=stdio|manual)
# 6. Si pidio operations.db
./fn ops init apps/<name> # o projects/<p>/apps/<name>
# 7. Generar icono
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
# 8. Indexar
./fn index
# 9. Compilar Windows
./fn run redeploy_cpp_app_windows <name> <dir> --build
# 10. Refrescar app_hub
./fn run refresh_app_hub
# 11. Auditoria
./fn doctor cpp-apps
[[ "<tag>" == *service* ]] && ./fn doctor services-spec
```
---
## Modo MODIFY — flujo
`/cpp-app modify <name>`
### Paso 0 — Localizar
```bash
# Buscar apps/<name>/ o projects/*/apps/<name>/
sqlite3 /home/lucas/fn_registry/registry.db \
"SELECT id, dir_path FROM apps WHERE name='<name>' AND lang='cpp';"
```
Si no existe: abortar, sugerir `/cpp-app` (sin args) para crear.
### Paso 1 — Mostrar config actual
```bash
mcp__registry__fn_show id="<id>"
cat <dir>/app.md
```
### Paso 2 — Que cambiar (multiSelect)
- `description` (1 linea)
- `icon.phosphor` o `icon.accent`
- `tags` (anadir/quitar; si toca `service` -> Paso 7 del create)
- `uses_functions` (anadir/quitar — recordar editar CMakeLists.txt)
- `panels` (anadir/quitar/renombrar)
- `service:` block (si tag=service)
- `e2e_checks`
- `domain`
- `rename` (cambia name, dir, IDs derivados, repo Gitea — operacion delicada, requiere doble confirmacion)
### Paso 3 — Aplicar cambios
Para cada cambio: usa `Edit` sobre los archivos correspondientes. NUNCA `Write` completo de `app.md` (preserva campos que no toques).
### Paso 4 — Post-acciones (segun lo que toco)
```bash
# Siempre
cd /home/lucas/fn_registry && ./fn index
# Si toco icon.* -> regenerar appicon
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
# Si toco trio o panels o uses_functions o cambia code:
./fn run redeploy_cpp_app_windows <name> <dir> --build
# Si toco description o icon o tags:
./fn run refresh_app_hub
# Si toco service: o tag service
./fn doctor services-spec
# Siempre al final
./fn doctor cpp-apps
```
---
## Reglas duras
- **NUNCA** crear `main.cpp` + `CMakeLists.txt` + `app.md` a mano. Siempre via `init_cpp_app_bash_pipelines` (regla `cpp_apps.md`).
- **NUNCA** poner el codigo en `cpp/apps/<n>/`. Solo `apps/<n>/` o `projects/<p>/apps/<n>/`.
- **NUNCA** dejar `app.md` sin el trio (description + icon.phosphor + icon.accent). Tarjeta del hub queda gris.
- **NUNCA** declarar funciones del registry en `uses_functions` sin listar su `.cpp` en `CMakeLists.txt` (drift detectado por `fn doctor uses-functions`).
- **NUNCA** usar `Restart=on-failure` en systemd unit de un service C++ — gotcha 2026-05-17 (`sqlite_api.service` cayo 20h). Default `Restart=always`.
- Despues de **cualquier** cambio en el trio: `regenerate_app_icons <name>` + `refresh_app_hub`.
---
## Auto-verificacion final
Tras crear o modificar, reportar al usuario:
```
=== app <name> ===
dir: <abs_dir>
domain: <d>
description: "<desc>"
icon: <phosphor> + <accent>
tags: [<csv>]
uses_functions: N funciones (<list_top_5>)
panels: N (<labels>)
e2e_checks: N checks
service: <si/no — port:<p> health:<h>>
Acciones ejecutadas:
[✓] scaffolder / edits
[✓] generate_app_icon
[✓] fn index (registry.db actualizado)
[✓] redeploy_cpp_app_windows (Desktop/apps/<name>/<name>.exe)
[✓] refresh_app_hub (tarjeta visible en hub)
[✓] fn doctor cpp-apps (limpio | N warnings)
Siguiente paso sugerido:
- Abrir app_hub_launcher en Windows y verificar tarjeta
- Anadir tests visuales si la app tiene paneles propios (cpp/PATTERNS.md §11)
```
$ARGUMENTS
+186
View File
@@ -0,0 +1,186 @@
---
name: fix-issue
description: Implementar un issue de dev/issues/ end-to-end. Crea rama, ejecuta tareas, bumpa version si toca modulos/framework/apps (via /version), tests, cierra issue, integra a master.
---
# /fix-issue
Ejecuta el flujo completo de implementacion/cierre de un issue de `dev/issues/`. Adaptado al stack del registry: Go (`-tags fts5 CGO_ENABLED=1`), Python (`python/.venv/bin/python3`), Bash, TypeScript (`pnpm`), C++ (`cmake`+`mingw-w64` toolchain).
## Inputs
```
/fix-issue <NNNN[a|b|c...]>
```
- `NNNN`: numero del issue (ej. `0107`).
- Si es sub-issue, sufijo letra: `0107a`, `0107b`, ...
Si no se proporciona, preguntar.
## Flujo obligatorio
### 1. Resolver el issue
- `dev/issues/<NNNN>-*.md` → si no existe, STOP.
- Si ya en `dev/issues/completed/`, STOP.
- Si es sub-issue, leer tambien el principal para contexto.
### 2. Leer y extraer
- Objetivo, tareas, arquitectura, prerequisitos, riesgos.
- Identificar archivos afectados — anotar si toca:
- `modules/<X>/` o `cpp/framework/` → bumpa version (paso 8).
- `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/` → indexer + `fn index` al cerrar.
- Apps en `apps/<X>/` o `projects/*/apps/<X>/` → requiere rama TBD (regla `apps_tbd.md`) **+ bumpa version per-app (paso 8)**. Si el issue toca multiples apps, una llamada `/version` por app.
- Registry meta (CLAUDE.md, rules, templates) → push directo a master OK.
### 3. Estrategia de rama
**Registry-only changes** (functions/types/docs/rules):
- Push directo a master OK. NO crear rama.
**Apps changes** (apps/, projects/*/apps/):
- Crear rama TBD:
```bash
git checkout master
git pull --rebase
git checkout -b issue/<NNNN>-<slug>
```
La rama es del registry. Si la app es sub-repo, ademas crear rama dentro del sub-repo.
**Modules/framework changes** (`modules/`, `cpp/framework/`):
- Rama TBD obligatoria (afecta a todas las apps que linkean).
### 4. Plan con TaskCreate
- Crear tarea por bloque logico del issue.
- Incluir SIEMPRE:
- Tarea de tests (unit + smoke).
- Tarea de `fn index` si toco metadata.
- Tarea de `/version` si toco `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/` (una llamada por target).
- Tarea de cleanup/docs.
### 5. Implementar
Reglas registry-first (CLAUDE.md):
- ANTES de escribir codigo reutilizable → `mcp__registry__fn_search` para encontrar lo que existe.
- Si falta funcion reutilizable → spawn `fn-constructor` (no escribir inline).
- Si patron se repite >2x → propose nueva funcion.
- NUNCA `sqlite3 registry.db "SELECT ..."` plano — usar MCP.
Convenciones del stack:
| Stack | Build/test |
|---|---|
| Go | `CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/` y `CGO_ENABLED=1 go test -tags fts5 ./...` |
| Python | `python/.venv/bin/python3 -m pytest <path>` |
| Bash | `bash -n <script>.sh` + tests inline |
| TypeScript | `cd frontend && pnpm build && pnpm test` |
| C++ (Linux) | `cmake --build build --target <app>` |
| C++ (Windows MinGW) | `cmake -B build/windows -DCMAKE_TOOLCHAIN_FILE=cpp/toolchains/mingw-w64.cmake && cmake --build build/windows --target <app>` |
Commits atomicos por bloque logico con prefijos: `feat:`, `fix:`, `test:`, `docs:`, `refactor:`, `chore:`. Mensajes en espanol. NO WIP.
### 6. Tests
Stack-dependent (ver arriba). Si tests pasan parcialmente con failures pre-existentes no causadas por la rama, documentar en cuerpo del commit/PR.
### 7. Feature flags (si aplica)
Si el issue forma parte de un feature multi-issue:
- Editar `dev/feature_flags.json` con el flag (desactivado).
- Activar el flag en el ultimo sub-issue del set.
Flag != WIP. Codigo detras de flag debe compilar + testear.
### 8. Version bump (si toco modulos/framework/apps)
**OBLIGATORIO si el issue toco** alguno de:
- `modules/<X>/` → bumpa `modules/<X>/module.md::version`.
- `cpp/framework/` → bumpa `modules/framework/module.md::version`.
- `apps/<X>/` → bumpa `apps/<X>/app.md::version`.
- `projects/<P>/apps/<X>/` → bumpa `projects/<P>/apps/<X>/app.md::version`.
```
/version <path> <major|minor|patch> "<reason>"
```
Reglas (modulos/framework):
- Major: breaking ABI/API publica.
- Minor: additive (nuevo helper, refactor interno sin cambio de API, nuevo miembro).
- Patch: bugfix puro.
Reglas (apps):
- Major: breaking observable (CLI args, schema BBDD propia, formato wire).
- Minor: feature aditiva visible (nuevo panel, endpoint, opcion).
- Patch: bugfix sin cambio observable, refactor interno, mejora perf.
**Una llamada `/version` por target afectado**. Si el issue toca 1 modulo + 2 apps -> 3 llamadas a `/version` (cada una con su `reason` y bump-type apropiado; pueden diferir).
Diff guard: cambios que solo tocan el `app.md` (correccion typo descripcion, anadir tag) NO requieren bump — son metadata, no comportamiento. Detectar con `git diff --name-only | grep -v '\.md$'` para decidir si hay cambio de codigo real.
`/version` solo edita + stage. NO commit. El bump va junto con el codigo correspondiente en el mismo commit (`feat:` o `fix:` o `refactor:`).
Si NO toco modulos/framework/apps, saltar este paso.
### 9. Cerrar el issue
Mover archivo:
```bash
mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/
```
Actualizar `dev/issues/README.md`:
- Link → `completed/<NNNN>-<slug>.md`
- Estado → `completado`
Si es feature multi-issue y este es el ultimo sub-issue:
- Flip flag en `dev/feature_flags.json` a `enabled: true` con `enabled_at: <YYYY-MM-DD>`.
- Verificar que todos los sub-issues estan en `completed/`.
### 10. Integrar
**Registry-only changes**: push directo a master.
**Apps/modules/framework changes**: `/full-git-push` o `/git-push` (merge --no-ff de la rama a master, push, delete rama).
### 11. Verificar post-cierre
- `fn index` — registry.db al dia.
- `fn doctor` (subcomandos relevantes: `artefacts`, `services`, `cpp-apps`, `uses-functions`).
- Si toco modulos: `fn doctor modules` (post 0107a) — 0 drift.
## Reglas criticas
- **Registry-first**: SIEMPRE buscar antes de escribir; delegar a `fn-constructor` antes que inline.
- **TBD para apps**: NUNCA push directo a master en apps. Rama corta, merge --no-ff.
- **TBD NO para registry**: push directo OK para functions/types/docs/rules.
- **`/version` obligatorio** si tocas modulos, framework o apps (con cambio de codigo real, no solo metadata). Si no, drift entre `version:` y `## Capability growth log` y se pierde trazabilidad.
- **Tests siempre**: no cerrar issue sin tests pasando (salvo failures pre-existentes documentados).
- **Commits atomicos**: 1 commit = 1 bloque logico. No mezclar `feat:` + `test:` en mismo commit.
- **Cerrar siempre**: nunca dejar issue implementado sin mover a `completed/` + actualizar README.
## Referenciado desde
- `.claude/commands/version.md` — bump semver de modulos.
- `.claude/commands/full-git-push.md` — push del registry + sub-repos.
- `.claude/rules/apps_tbd.md` — politica de TBD por tipo de cambio.
## Ejemplo: implementar 0107c (refactor data_table)
```
/fix-issue 0107c
1. Resolver: dev/issues/0107c-split-data-table.md ✓
2. Extraer: refactor 4777 LOC → 6 sub-funciones. Toca modules/ → /version obligatorio.
3. Rama: issue/0107c-split-data-table desde master.
4. Plan: 8 tareas (lectura + 6 sub-funciones + entrypoint thin + version bump).
5. Implementar: spawn fn-constructor en paralelo si hay >1 sub-funcion independiente.
6. Tests: build + smoke + primitives_gallery --capture diff.
7. Flag: parte de modules-v2, NO activar todavia (espera 0107a-f cerrar).
8. /version modules/data_table major "split data_table.cpp into 6 sub-functions"
9. Cerrar: mv → completed/ + README.
10. /git-push.
11. fn index + fn doctor modules → 0 drift en consumidores limpiados.
```
+131
View File
@@ -0,0 +1,131 @@
---
description: "Gestiona flows (casos de uso multi-app reutilizables) en dev/flows/. Subcomandos: create, list, show, status, done. Runner automatizado en fase 2."
---
# /flow — Gestionar flows del registry
Flows = casos de uso end-to-end que prueban / ejercitan el sistema multi-app. Viven en `dev/flows/NNNN-<slug>.md`. Cada flow describe Goal + Flow steps + Acceptance checkboxes + Telemetria.
**OBLIGATORIO antes de `create`**: lee `dev/flows/AGENT_GUIDE.md`. Define donde buscar piezas (capability groups, FTS por tag, apps existentes, vaults), reglas duras para no inventar IDs, y plantilla de razonamiento para recomendar extractor / transformer / sink / scheduler / notify por flow.
Cada flow nuevo cita IDs reales del registry. Si una pieza falta, escribir `FALTA: crear <id>` en la tabla correspondiente. Nada de inventar nombres.
Diferencia con `dev/issues/`:
- Issues = bugs / features de implementacion.
- Flows = trabajos reutilizables que cruzan varias apps.
## Sintaxis
```
/flow create <slug> # nuevo flow desde template, ID auto
/flow list # tabla resumen
/flow show <NNNN> # imprime contenido + acceptance %
/flow status <NNNN> # status + acceptance % + ultima run
/flow done <NNNN> [--notes "..."] # cierra flow (status=done, mueve a completed/)
/flow run <NNNN> # fase 2 — runner automatizado (NO IMPLEMENTADO)
```
## Implementacion por subcomando
### `create <slug>`
Pasos:
1. Valida `<slug>` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error.
2. Comprueba que no existe ya: `ls dev/flows/*-<slug>.md`. Si existe, error.
3. Calcula siguiente ID libre:
- `ls dev/flows/*.md dev/flows/completed/*.md | grep -oE '^dev/flows/(completed/)?[0-9]{4}' | sort -u | tail -1`
- Suma 1, zero-pad a 4 digitos.
4. Lee `dev/flows/template.md`.
5. Sustituye `<slug>`, `NNNN`, `YYYY-MM-DD` (hoy).
6. Escribe `dev/flows/NNNN-<slug>.md`.
7. Append fila a `dev/flows/INDEX.md` (mantener orden por ID asc).
8. Reporta path nuevo + recordatorio "edita Goal / Flow / Acceptance".
### `list`
Lee `dev/flows/INDEX.md` y lo imprime tal cual. Si flag `--pending` solo pending, `--done` solo done, `--app <name>` filtra por app.
Tambien anade columna `Accept%` calculada desde body:
- Para cada flow .md, cuenta `[ ]` y `[x]` en seccion `## Acceptance`.
- `% = checked / total * 100` redondeo entero.
### `show <NNNN>`
`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error.
### `status <NNNN>`
Imprime resumen del frontmatter + acceptance %:
```
=== flow 0001 ===
name: hn-top-stories
status: pending
risk: low
priority: high
apps: navegator_dashboard, dag_engine, data_factory, agents_and_robots
acceptance: 2/6 (33%)
updated: 2026-05-16
Pending checks:
- [ ] Recipe creada y validada
- [ ] DAG corre OK 2 veces consecutivas via scheduler
- [ ] data_factory.runs tiene >=2 entries
- [ ] Schema extraido cubre 6/6 fields
```
### `done <NNNN> [--notes "..."]`
Pasos:
1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente".
2. Edita frontmatter: `status: done`, `updated: <hoy>`.
3. Si `--notes`, append a seccion `## Notas`.
4. `git mv dev/flows/NNNN-<slug>.md dev/flows/completed/`.
5. Actualiza `dev/flows/INDEX.md`: cambia status del flow + mueve fila a seccion Completed (mantener tabla principal solo con pending/running/failed/deferred).
### `run <NNNN>` — FASE 2 (NO IMPLEMENTADO AUN)
Hoy: imprime `/flow run no implementado todavia. Sigue los pasos manualmente y marca acceptance con sed/edit.`
Diseño futuro:
- Parsea `## Flow` en pasos.
- Cada paso tipo `function: <id>` -> ejecuta `./fn run <id>`.
- Cada paso tipo `cmd: <bash>` -> ejecuta subprocess.
- Texto libre -> "MANUAL: <text>" + pause user input.
- Persistencia ejecuciones en `dev/flows/runs/<id>-<timestamp>.jsonl`.
- Update acceptance checkboxes automaticamente segun heuristics (count runs en data_factory, etc.).
## Conventions
- Numeracion 0001+, propia (no comparte con `dev/issues/`).
- Status: `pending | running | done | failed | deferred`.
- Risk: `low` (publico) | `medium` (auth no sensible) | `high` (datos personales).
- Apps listadas en frontmatter — `/flow list --app navegator_dashboard` filtra.
- Acceptance es la fuente de verdad del progreso.
## Output style
Caveman. Tablas markdown. Sin emojis. Sin verbosidad.
Errores: 1 linea con el problema + sugerencia.
## Ejemplos
```
/flow create reddit-sentiment-tracker
# crea dev/flows/0008-reddit-sentiment-tracker.md
# anade fila a INDEX
/flow list --pending
# muestra solo flows no cerrados
/flow status 0001
# acceptance 0/6, todos los checks pendientes
# Tras correr el flow manualmente:
# editas el .md, marcas [x] los checks completados
/flow status 0001
# acceptance 6/6
/flow done 0001 --notes "smoke pass; LLM tardo 14s; recipe robusta"
# mueve a completed/, marca status=done
```
+14
View File
@@ -152,6 +152,20 @@ Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo:
cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose
```
### 5b. MEMORIZE — anadir cada funcion nueva a MEMORY.md (issue 0087 pieza 6)
Por cada funcion creada con exito, llama:
```bash
bash "$ROOT/.claude/scripts/append_fn_to_memory.sh" "<fn_id>" "<one-line purpose>"
```
El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_<id>.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-<id>](reference_fn_<id>.md) — <purpose>`. Asi proximas sesiones cargan MEMORY.md y ven el catalogo de funciones recien creadas sin segunda lookup.
`purpose` = 1 frase derivada del `description` del .md de la funcion (max 80 chars). Si description es larga, recorta. Ejemplo:
- fn_id: `parse_http_log_go_infra`
- purpose: "parsea log Apache/Nginx a struct; pure"
Reporta:
- N funciones nuevas creadas (con IDs)
- N proposals nuevas en `registry.db.proposals`
+93
View File
@@ -0,0 +1,93 @@
---
description: "Gestiona issues del registry en dev/issues/. Subcomandos: list, show, status, board, dep, roadmap, tag, done, stale, create. Frontmatter YAML canonico (issue 0100)."
---
# /issue — Gestionar issues del registry
Issues viven en `dev/issues/NNNN-<slug>.md` con frontmatter YAML canonico (id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags).
Allowlists en `dev/TAXONOMY.md` (no inventar valores).
Diferencia con `dev/flows/`:
- **Issues** = bugs, features, refactors, chores, epics de implementacion.
- **Flows** = casos de uso end-to-end multi-app.
## Sintaxis
```
/issue list [--domain X] [--type Y] [--status Z] [--prio P] [--epic NNNN]
/issue show NNNN
/issue status NNNN # acceptance % + estado deps
/issue board # kanban pendiente/in-progress/bloqueado/done
/issue dep NNNN # arbol bloquea/depende
/issue roadmap NNNN # epic + sub-IDs (NNNNa, NNNNb, ...)
/issue tag NNNN +X -Y # mantenimiento tags/domain
/issue done NNNN # mueve a completed/, valida deps
/issue stale [--days 30]
/issue create <slug> --type T --domain D [--prio P] [--depends NNNN]
```
## Implementacion
**Fase 1 (manual via Claude):**
El agente lee `dev/issues/*.md`, parsea frontmatter YAML con `yaml.safe_load`, aplica el filtro, imprime tabla.
```python
import yaml, pathlib, re
issues = []
for f in pathlib.Path("dev/issues").glob("*.md"):
if f.name in {"README.md", "template.md"}: continue
txt = f.read_text()
m = re.match(r"^---\n(.*?)\n---", txt, re.S)
if not m: continue
fm = yaml.safe_load(m.group(1)) or {}
fm["_path"] = str(f)
issues.append(fm)
# filter + print
```
**Fase 2 (cuando 0101 dev_console exista):**
Cada subcomando se mapea a `./apps/dev_console/dev_console issue <subcomando> $ARGS`.
## Subcomandos clave
### `list`
Imprime tabla `id | title | status | type | domain | priority | depends_pending`. Filtrable por flags.
### `show NNNN`
Read directo del .md + render del frontmatter como tabla + body como markdown.
### `status NNNN`
Cuenta checkboxes en `## Acceptance` + chequea si todos los `depends` estan en `status: completado`. Si alguno no, marca `bloqueado`.
### `board`
Tabla 4 columnas (pendiente / in-progress / bloqueado / completado_hoy). Card por issue: id + title + prio. Status `bloqueado` se calcula on-the-fly desde `depends`.
### `roadmap NNNN`
Si `type: epic`: lista sub-issues `NNNNa`, `NNNNb`, etc. con su estado. Si no epic: error "not an epic".
### `done NNNN`
1. Lee frontmatter.
2. Verifica todos `depends` cerrados (sino, error).
3. Cuenta `## Acceptance` 100% (sino, error).
4. `git mv dev/issues/NNNN-*.md dev/issues/completed/`.
5. Actualiza `status: completado` + `updated: today`.
### `create <slug> --type T --domain D`
Genera siguiente ID libre (max existing + 1, zero-padded 4). Scaffold desde plantilla minima con frontmatter rellenado.
## Reglas
- Domain debe estar en `dev/TAXONOMY.md` allowlist.
- Scope/type/priority idem.
- `id` siempre string `"NNNN"` (zero-padded, sub-IDs con sufijo `a-z`).
- Modificar frontmatter SIEMPRE preserva campos no tocados (no overwrite).
+170
View File
@@ -0,0 +1,170 @@
---
name: version
description: Bumpear semver de un modulo, framework, paquete o app del registry. Edita <target>.md::version + ## Capability growth log. NO commitea.
---
# /version
Bumpea la version de un **modulo, framework, paquete o app** del registry siguiendo SemVer estricto y mantiene el `## Capability growth log` sincronizado con `<target>.md::version`.
Disenado para usarse desde `/fix-issue` cuando el cambio afecte:
- `modules/<X>/` (cualquier modulo C++) — edita `module.md`
- `cpp/framework/` — edita `modules/framework/module.md`
- `apps/<X>/` o `projects/<P>/apps/<X>/` — edita `app.md`
- Otros paquetes versionados con `<target>.md` y campo `version:`
## Inputs
```
/version <path> <major|minor|patch> "<reason>"
```
- `<path>`: directorio del target (ej. `modules/data_table`, `cpp/framework`, `apps/chart_demo`, `projects/fn_monitoring/apps/registry_dashboard`).
- `<major|minor|patch>`: tipo de bump SemVer.
- `<reason>`: 1-frase humana — lo que cambia. Se inserta en el log.
## Resolucion del archivo target
| Path empieza por | Archivo a editar |
|---|---|
| `modules/` | `<path>/module.md` |
| `cpp/framework` | `modules/framework/module.md` |
| `apps/` | `<path>/app.md` |
| `projects/*/apps/` | `<path>/app.md` |
| `projects/*/analysis/` | `<path>/analysis.md` |
Si no encuentra archivo target -> ERROR.
## Reglas SemVer
### Modulos / framework
| Bump | Cuando |
|---|---|
| `major` | Cambios breaking en API publica: firma de entry function, layout de State struct expuesto, eliminacion de members, cambio incompatible de comportamiento. |
| `minor` | Adiciones backwards-compatible: nuevo evento opt-in, nuevo renderer, nuevo helper, nuevo miembro. |
| `patch` | Bugfix sin cambio de API. |
Refactor interno SIN cambio de API publica -> `minor` (no major).
### Apps
| Bump | Cuando |
|---|---|
| `major` | Breaking observable por usuarios: CLI args incompatibles, schema BBDD propia rompe lectores viejos, formato wire (HTTP/gRPC) incompatible, eliminacion de panel/feature que la gente usaba. |
| `minor` | Feature aditiva: nuevo panel, nuevo endpoint, nueva opcion CLI, nueva tab, mejora visible no rompedora. |
| `patch` | Bugfix sin cambio observable. Refactor interno. Mejoras de perf. |
Bump de **dependencia** (modulo/funcion del registry) que mejora la app pero la app no cambia su API -> `patch` (la app no es responsable de la mejora; el modulo si).
## Flujo
### 1. Validar input
- `<target_file>` existe -> si no, ERROR.
- Bump type en {major, minor, patch} -> si no, ERROR.
- Reason no vacia -> si no, ERROR.
### 2. Leer version actual
Parsear frontmatter. Buscar `version: X.Y.Z`. Si no existe:
- Para `module.md` -> ERROR "module.md sin campo version".
- Para `app.md` -> asumir `0.1.0` (baseline) e insertar el campo despues de `domain:`.
### 3. Calcular proxima version
```
1.4.0 + major = 2.0.0
1.4.0 + minor = 1.5.0
1.4.0 + patch = 1.4.1
```
Major bump -> minor y patch a 0. Minor bump -> patch a 0.
### 4. Editar `<target_file>`
Cambiar linea `version: <old>` por `version: <new>`.
### 5. Anadir entrada a `## Capability growth log`
Insertar al inicio de la lista (lineas posteriores al header `## Capability growth log`):
```markdown
- v<new> (<fecha YYYY-MM-DD>) — <reason>
```
Si la seccion no existe -> crearla al final del archivo antes de `## Notes` (o al final si no hay Notes).
### 6. Verificar drift de members (solo modulos, opcional)
Si la herramienta `fn doctor modules` existe (post 0107a) y el target es modulo:
- Compara `members:` actual vs ultima version registrada en `registry.db::modules_history`.
- Si hay diff en members y bump es `patch` -> WARNING.
- Si hay diff en API publica y bump no es `major` -> ERROR (require `--force`).
No aplica a apps (no tienen `members:`).
### 7. Stage en git
`git add <target_file>`. NO commit. El commit final lo hace el flujo padre.
### 8. Reportar
```
/version apps/chart_demo minor "anade tab radar chart"
apps/chart_demo/app.md
version: 1.2.0 -> 1.3.0
## Capability growth log: + v1.3.0 (2026-05-18) — anade tab radar chart
Staged. NO committed.
Next: terminar el fix-issue y hacer commit con el resto de cambios.
```
## Reglas criticas
- **NUNCA commit**. `/version` solo edita + stage. El commit lo hace el flujo padre (`/fix-issue`, `/git-push`).
- **NUNCA saltar version**. No 1.4.0 -> 1.4.2 directo.
- **NUNCA bajar version**. Si rollback, crea nueva version superior con comportamiento viejo restaurado.
- **fecha = HOY** (`date +%Y-%m-%d`).
- **reason** comprensible sin contexto del PR actual.
## Referenciado desde
- `/fix-issue` — al detectar cambios en `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/`, sugiere ejecutar `/version` antes del commit final.
- `.claude/rules/cpp_apps.md` — politica de bump.
- `dev/issues/0107-modules-standardization.md` — origen del flujo (modulos).
## Ejemplos
```
# Bug fix en data_table (modulo)
/version modules/data_table patch "fix off-by-one en seleccion multi-row con shift+click"
# -> 1.4.0 -> 1.4.1
# Feature opt-in en framework
/version cpp/framework minor "anade cfg.auto_dockspace para overlay de paneles flotantes"
# -> 1.1.0 -> 1.2.0
# Feature en app C++
/version apps/chart_demo minor "anade tab radar chart con datos sinteticos"
# -> 1.2.0 -> 1.3.0
# Bug fix en app de proyecto
/version projects/fn_monitoring/apps/registry_dashboard patch "fix tooltip que mostraba duration_ms en segundos"
# -> 0.4.1 -> 0.4.2
# Breaking en app: cambia schema de su BBDD propia
/version apps/kanban major "cards.assignee_id pasa a ser TEXT[] (era TEXT); requiere migracion 008"
# -> 1.0.0 -> 2.0.0
```
## Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Editar `version:` a mano sin `## Capability growth log` | Drift entre version y log; nadie sabe que cambio. |
| Bumpear major en app por refactor interno | Confunde al usuario; refactor es patch. |
| Patch para feature visible | Usuario no se entera que esta disponible. |
| Reason "cambios varios" / "mejoras" | Inutil para auditar. Una frase concreta. |
| Bump de app sin tocar codigo de la app (solo dep) | Bump va al modulo, no a la app. |
+67
View File
@@ -0,0 +1,67 @@
---
description: "Vista cross-cutting de issues + flows. Subcomandos: today, weekly, search, dashboard. Mezcla los dos universos en una lista priorizable."
---
# /work — Vista cross-cutting issues + flows
Issues = trabajo de implementacion. Flows = casos de uso multi-app. `/work` los muestra juntos para responder "que hago ahora" sin saltar entre dos sitios.
## Sintaxis
```
/work today # top items prio alta + deps satisfechas (issues + flows)
/work weekly # review semanal: closed vs planeados
/work search "texto" # FTS sobre issues + flows + completed
/work dashboard # JSON consumible por tab Work (issue 0102)
```
## Implementacion
**Fase 1 (manual via Claude):**
El agente lee `dev/issues/*.md` + `dev/flows/*.md`, parsea frontmatter YAML, ordena por:
1. `priority: alta` primero.
2. `status: pendiente` con `depends` todos `completado` (no bloqueados).
3. Items con DoD/Acceptance >=80% (a punto de cerrar).
4. Fecha `updated` mas reciente.
Imprime tabla unificada:
```
KIND | ID | TITLE | PRIO | STATUS | NEXT STEP
issue| 0099 | datahub app launcher | alta | pendiente | revisar deps
flow | 0001 | hn-top-stories | high | pending | cerrar DoD user-facing
issue| 0100 | migrate issue frontmatter | alta | pendiente | ejecutar pipeline
...
```
**Fase 2 (cuando 0101 dev_console exista):**
`./apps/dev_console/dev_console work <subcomando> $ARGS`.
## Subcomandos
### `today`
Filtro: `priority in (alta, media)` + `status: pendiente` + dependencias resueltas. Max 10 items. Si hay >10, prioriza `alta` y avisa "N items pendientes en cola".
### `weekly`
Git log `--since='1 week ago'` sobre `dev/issues/completed/` y `dev/flows/completed/` -> tabla de items cerrados. Comparado con `created: <esta semana>` -> ratio in/out.
### `search "texto"`
`grep -ri` sobre `dev/issues/` + `dev/flows/` (incluido completed/), filtra por title/body. Output: `path:line: match`.
### `dashboard`
Output JSON estructurado para consumo por tab Work del `registry_dashboard` (issue 0102). Estructura:
```json
{
"issues": {"pendiente": [...], "in-progress": [...], "bloqueado": [...], "completado_24h": [...]},
"flows": [{"id": "0001", "dod_percent": 50, "user_facing_percent": 0, "...": ...}],
"telemetry": {"calls_24h": N, "violations_24h": N, "pending_proposals": N}
}
```
+4
View File
@@ -21,6 +21,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo |
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
@@ -34,3 +35,6 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 |
| 32 | [../../dev/TAXONOMY.md](../../dev/TAXONOMY.md) | Allowlist canonica para dominios/tipos/scopes/estados/prioridades + flow patterns. Aplica a `dev/issues/` y `dev/flows/`. Issues 0100 + 0103 |
| 33 | [project_commands.md](project_commands.md) | Slash commands por project (`.claude/commands/<project>/`) expuestos via symlink. Desde fn_registry: `/<project>:foo`. Desde el project: `/foo`. Sin colision. |
+74
View File
@@ -0,0 +1,74 @@
## Apps son sub-repos Gitea independientes — gotcha al usar worktrees
**Regla operativa critica** descubierta el 2026-05-18 durante implementacion del flow 0008.
### El gotcha
`apps/*/` esta en `.gitignore` del repo `fn_registry`. Cada app es **su propio repo Gitea** en `dataforge/<app_name>` con su `.git/` dentro de `apps/<app_name>/`. Esto significa:
- Cuando un agente trabaja en un git **worktree** del repo padre y crea `apps/<nueva_app>/`, los archivos viven SOLO en el working directory del worktree.
- Como `apps/*/` esta gitignored en el repo padre, los archivos **no se pueden commitear** al worktree del repo padre.
- Cuando se hace `git worktree remove --force worktrees/<slug>/`, el working directory entero se borra — **el codigo de la app desaparece**.
**Consecuencia**: una app creada dentro de un worktree del repo padre se pierde al limpiar el worktree salvo que se haya promovido a su propio sub-repo Gitea ANTES.
### El patron correcto al crear apps en worktrees
```bash
# 1. Agente trabaja en worktree del repo padre
cd /home/lucas/fn_registry/worktrees/<slug>
# 2. Scaffold la app via pipeline canonico
./fn run init_cpp_app <name> # apps C++
# o ./fn run init_jupyter_analysis ... # analysis
# o crear apps/<name>/ a mano (Go service, etc.)
# 3. ANTES de salir del worktree: inicializa la app como sub-repo
cd apps/<name>
git init -b master
git add -A
git -c user.email="agent@fn_registry" -c user.name="agent" \
commit -m "feat: initial scaffold of <name>"
# 4. Trabajo continua en sub-repo (commits dentro de apps/<name>/.git)
# 5. Cerrar issue en repo padre (mv .md a completed/), commit del padre con cambios en cpp/CMakeLists.txt, etc.
```
Cuando el humano corre `/full-git-push` despues del merge, el script `ensure_repo_synced_bash_infra` detecta que `apps/<name>/.git` existe + no tiene remote + crea repo Gitea en `dataforge/<name>` + pushea master.
### Que ESTA SI versionado en el repo padre
- `cpp/CMakeLists.txt` (el `if(EXISTS ...) add_subdirectory(apps/<name>) endif()`).
- `dev/issues/completed/<NNNN>-<slug>.md` (cierre del issue).
- `docs/capabilities/*.md` si la app aporta a un capability group.
- `dev/feature_flags.json` si introduce flags.
Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps/<name>/.git` independiente.
### Sintomas de la perdida
Si limpias el worktree y luego corres `ls apps/<name>/`, devuelve "No such file or directory" pese a que el issue aparece cerrado en `dev/issues/completed/`. **Patron** = scaffold sin sub-repo init = trabajo perdido.
### Recovery si pasa
1. Re-crear worktree desde master.
2. Re-spawn agente con instruccion explicita: **`git init` dentro de la app antes de terminar**.
3. NO eliminar el worktree hasta confirmar que `apps/<name>/.git` esta inicializado con al menos un commit.
### Aplica tambien a analysis
`analysis/*/` y `projects/*/analysis/*/` siguen mismo patron (cada analysis es repo Gitea). El pipeline `init_jupyter_analysis_bash_pipelines` ya hace `git init` automatico — por eso no hubo perdidas alli. Las apps C++/Go scaffolded a mano NO inicializan el sub-repo automaticamente — es responsabilidad del agente.
### Lo que aprende `parallel-fix-issues`
El template del prompt de cada agente DEBE incluir la instruccion:
> "Si tu issue crea una app nueva en `apps/<name>/`, inicializa el sub-repo (`cd apps/<name> && git init -b master && git add -A && git commit ...`) antes de terminar. Sin esto, `apps/*` esta gitignored y el codigo se perdera cuando el orquestador limpie el worktree."
Aplicar este parrafo al template del skill — ver `.claude/skills/parallel-fix-issues/SKILL.md` (o equivalente).
### Relacion con otras reglas
- [[apps_tbd]] — TBD en apps, esta regla complementa con el patron de sub-repo init.
- [[artefactos]] — apps son artefactos, esta regla especifica gotcha de su sub-repo.
- [[apps_vs_functions]] — apps en `apps/`, esta regla refuerza por que apps/* gitignored.
+102
View File
@@ -0,0 +1,102 @@
## Bucle autonomo (`fn-orquestador` + `/autonomous-task`) — issue 0069
`fn-orquestador` recorre el ciclo reactivo (CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR) sin intervencion humana, hasta convergencia (suite verde), estancamiento (no progreso N iteraciones), timeout, o tope de iteraciones. Trabaja SIEMPRE en sandbox `auto/<issue>`, NUNCA merge a master.
### Cuando se invoca
- Skill `/autonomous-task <issue_id>` (humano lanza explicitamente).
- Cron / dag_engine (`schedule:` en YAML; planificable, no implementado por defecto).
- NO se invoca como reaccion a hooks ni a fallos de tests "en caliente". Siempre tarea explicita.
### Reglas duras
1. **Sandbox obligatorio**: rama `auto/<issue_id>-<slug>`. Si la rama existe -> reset hard contra master y reanudar. NUNCA commits a master, NUNCA push --force-with-lease a master.
2. **Paths protegidos**: respetar `dev/autonomous_protected_paths.json` exactamente. Cualquier intento de modificar un path protegido aborta la iteracion y registra `task_runs.status='aborted_protected_path'`.
3. **Filtro de proposals auto-aplicables**: el orquestador SOLO aplica proposals que cumplen:
- `kind in (bug_fix, e2e_check_add, doc_update, capability_tag_add)` -> auto-aplicable.
- `kind in (new_function, deprecate_function, refactor, schema_change)` -> NO auto-aplicable (queda `pending` para humano).
- `priority in (low, medium)` -> auto-aplicable. `high|critical` -> requiere humano salvo override `--allow-high`.
4. **Watchdog**: si la metrica de progreso (`checks_pass / checks_total`) no sube en `N=3` iteraciones consecutivas -> abort. Registrar `task_runs.status='stalled'`.
5. **Tiempo**: cada `task_run` con timeout default 30 min. Override con `--timeout-min N` hasta max 4h.
6. **Idempotencia**: re-ejecutar `/autonomous-task <id>` sobre la misma issue reanuda desde la ultima iteracion exitosa, NO reinicia desde cero (lookup en `task_runs` por `issue_id`).
7. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary}`. El humano puede leer el log entero para auditar.
8. **No self-modification**: orquestador NUNCA modifica `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`. Reforzado en `autonomous_protected_paths.json`.
9. **NUNCA paths absolutos fuera del worktree**. Refuerzo del piloto 1 (2026-05-15): el orquestador uso `/home/lucas/fn_registry/bash/functions/...` para fixear hooks bash y contamino el repo principal. Solucion correcta: fix vive solo en el worktree. Post-cada-iteracion: `git -C <main_repo> status --short` debe permanecer igual al baseline; cualquier diff = `status=sandbox_breach` -> ABORT.
10. **Pre-commit hooks compartidos**. Worktrees comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecutara la version de main. Si el hook bloquea progreso por bug en main: aplica el fix EN EL WORKTREE (commit en auto/*); si el bug del hook excede scope: `git commit --no-verify` para ESE commit con `task_runs.events_json[].decision="skip_hook"` + razon. NO editar main.
### Sub-repos vs worktree padre
Cuando el issue toca `app.md` o codigo dentro de `apps/<name>/`, `projects/<p>/apps/<name>/`, `cpp/apps/<name>/`, o `analysis/<a>/` — estos directorios son **sub-repos Gitea independientes** y estan `.gitignore`d en el repo padre `fn_registry` (regla `apps_subrepo.md`). El orquestador:
- **Crea worktree padre** `auto/<issue>` en `/tmp/fn_orq_<issue>_<ts>/` por protocolo, **pero no escribe alli** porque los cambios no se versionan en el padre.
- **Opera DIRECTAMENTE en el sub-repo** de la app/analysis target. Branch `auto/<issue>-<slug>` se crea dentro de `apps/<name>/.git`, NO en el padre.
- **PR draft sale al sub-repo** en `dataforge/<name>` (NO a `dataforge/fn_registry`). Humano revisa+mergea en el sub-repo.
- **Worktree padre queda vacio** y se limpia normal con `git worktree remove` al terminar.
Validado en piloto 0120 (`add_e2e_check` sobre `chart_demo`): PR creado en `dataforge/chart_demo/pulls/1`, sanity check del main repo `fn_registry` confirmo cero contaminacion.
Si el issue toca AMBOS lados (codigo del registry padre + app de sub-repo), el orquestador commitea separado: cambios del padre en `auto/<issue>` (worktree padre), cambios de la app en `auto/<issue>-<slug>` (sub-repo). Dos PRs draft. Humano coordina merge.
### Gitea API vs `gh`
Pre-condicion `gh auth status` es smoke check (target github.com). Mecanismo real de PR es `curl` a Gitea API:
```bash
GITEA_TOKEN=$(pass gitea/dataforge-git-token | head -n1)
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"...","head":"auto/<issue>-<slug>","base":"master","draft":true,"body":"..."}' \
"https://gitea-.../api/v1/repos/dataforge/<repo>/pulls"
```
Validado en pilotos 0076 y 0120.
### Estructura task_run
Migration `fn_operations/migrations/006_task_runs.sql`. Campos minimos: `id`, `issue_id`, `branch`, `started_at`, `finished_at`, `status` (`running|done|failed|aborted_protected_path|stalled|timeout`), `iterations`, `checks_pass`, `checks_fail`, `proposals_applied_json`, `proposals_skipped_json`, `events_json`, `final_diff_sha`.
### Fases por iteracion
```
loop:
1. fn-constructor (Read+Edit+Write+Bash limitados) - aplica fix segun ultima proposal seleccionada
2. fn-executor - corre build + tests + smoke
3. fn-recopilador - audita operations.db de la app
4. fn-analizador - corre e2e_checks (registra e2e_runs)
5. SI todos los checks pasan -> commit + push rama + abre PR. status=done. exit.
6. SI no progreso N iteraciones -> abort. status=stalled.
7. fn-mejorador - crea proposals desde fallos
8. orquestador filtra proposals auto-aplicables -> selecciona la primera -> goto 1.
```
### Output al humano
```
=== /autonomous-task 0068 ===
task_run_id: run_e2e_a1b2c3
branch: auto/0068-e2e-validation
iterations: 4
status: done
checks_pass: 8/8
proposals_applied: 3 (run_e2e_run_001, run_e2e_run_002, run_e2e_run_003)
proposals_skipped: 1 (refactor — needs human review)
PR: https://gitea.../pulls/42
```
### Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| Mergear `auto/<issue>` a master sin PR + humano | Salta gate, riesgo de regresion |
| Auto-aplicar proposal `kind=refactor` | Cambios sistemicos requieren revision |
| Modificar `go.sum`, `package-lock.json`, `uv.lock` | Cambios de deps requieren CVE/license review |
| Bucle infinito sin watchdog | Coste descontrolado de tokens |
| Borrar archivos sin backup en `task_runs.events_json` | Pierde auditoria |
| Override de paths protegidos via env var | Bypass de seguridad |
### Relacion con otras reglas
- [[e2e_validation]] — fn-analizador (fase 4) lee el contrato `e2e_checks` que el orquestador usa como gate.
- [[apps_tbd]] — el orquestador opera en rama `auto/*`, no exenta de TBD.
- [[feature_flags]] — si el fix no esta terminado, el orquestador puede meterlo detras de flag OFF antes de PR.
- [[registry_calls]] — toda invocacion del orquestador y sub-agentes pasa por MCP/`fn run`/heredoc canonico, registrada en call_monitor.
+233 -12
View File
@@ -20,14 +20,14 @@ Razones:
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
### 1. Ubicacion
### 1. Ubicacion (issue 0096 estandarizada)
| Caso | Donde vive |
|---|---|
| App independiente | `cpp/apps/<nombre>/` |
| App independiente | `apps/<nombre>/` |
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
NUNCA en `cpp/apps/<nombre>/` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
### 2. Estructura minima
@@ -84,6 +84,7 @@ Plantilla minima para apps C++:
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
version: 0.1.0 # semver per-app, bumped via /version
description: "Frase corta — lo que hace y por que existe."
tags: [imgui, ...] # si es service, anadir 'service'
uses_functions: # IDs del registry — el indexer NO deduce C++
@@ -102,6 +103,7 @@ Reglas:
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
- `repo_url` apunta al sub-repo en Gitea (ver §6).
- `version`: semver per-app. Baseline `0.1.0` para apps nuevas. Bump obligatorio via `/version apps/<name> {major|minor|patch} "<reason>"` cuando `/fix-issue` toque codigo de la app. Trazabilidad humana en seccion `## Capability growth log` al final del `app.md` (una linea por bump). Ver `.claude/commands/version.md`.
### 5. Registro en `cpp/CMakeLists.txt`
@@ -189,20 +191,105 @@ WMs). Activado por defecto, sin opt-in:
con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame).
2. **Per-frame viewport sync** al inicio del main loop — cubre viewports
secundarios (paneles drag-out) que la backend crea dinamicamente.
3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE`
/ `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras
el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`,
replicando el contrato del title-bar drag native (DefWindowProc bloquea
el hilo, DWM compositor mueve el framebuffer existente).
3. **Win32 WndProc subclass per HWND** (`#ifdef _WIN32`) — observa
`WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada
drag. El subclass se instala en la ventana principal Y en cada HWND
secundario que el backend de ImGui crea cuando un panel se arrastra fuera
del main (escaneo per-frame de `pio.Viewports`). Mientras el bracket esta
abierto en CUALQUIER HWND propio, el main loop SKIPEA `render_fn` +
`glfwSwapBuffers` globalmente, replicando el contrato del title-bar drag
native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer
existente). El flag `g_in_sizemove` es global a proposito: una sola
sesion de sizemove externo pausa todo el render para que ninguna ventana
compita con el OS.
Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases:
Estado del subclass:
- `g_subclassed` = `unordered_map<HWND, WNDPROC>`. Chain a la proc
original via `CallWindowProcW`.
- `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa).
- Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada
`pio.Viewports[i]->PlatformHandle` nuevo.
- `uninstall_sizemove_subclass_all()` restaura cada HWND al exit.
#### Iconified main no pierde paneles flotantes (2026-05-16)
El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO
el frame loop. Con multi-viewport activo eso significa que
`ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de
refrescar los viewports secundarios — los floating panels aparecen congelados
o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta
viewports secundarios primero; si hay alguno, fall-through al frame normal
(la swap del main HWND minimizado es harmless, los contexts GL secundarios
siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`.
#### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16)
WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`):
- `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` +
`PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo.
- `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/
BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite
`PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo.
Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a
cada viewport flotante porque el subclass per-frame ya cubre todos los HWND.
El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa
render → cero jitter automatico, mismo contrato que el title-bar drag.
**Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut
UI. Aceptable porque Alt-modifier en clicks UI es muy raro.
#### Title-bar-only move para ImGui windows (2026-05-16)
`fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico
para viewports secundarios: un viewport flotante = OS window borderless con
UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas
arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport
entero, el OS window sigue al cursor sin modifier. Con el flag, floating
panels obedecen el contrato "solo header arrastra" (igual que main que tiene
title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido
antes por el subclass).
#### Test observability — `fn::internal::*` (2026-05-16)
Counters monotonicos para validar el subclass desde tests headless,
zero-cost en prod:
```cpp
namespace fn::internal {
int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE
int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido
int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido
int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN
void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests
}
```
En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan
pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el
modal de Windows. Path real en prod sigue posteandolos.
Tests: `apps/altsnap_jitter_test/` corre seis fases:
- `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta
`vp->Pos` sigue OS dentro de 1px.
- `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` +
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta
que `render()` no se llama durante el bracket.
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el
HWND principal, asserta que `render()` no se llama durante el bracket.
- `p3.secondary` (Windows): fuerza viewport secundario
(`ConfigViewportsNoAutoMerge=true`), localiza su HWND y repite el bracket
sobre el. Valida que el subclass per-viewport tambien pausa el render.
- `p4.minimize` (Windows): state machine 4 steps — captura
`IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow +
glfwRestoreWindow`. Asserta los 3 estados vivos y `renders_iconified > 0`.
- `p5.alt_rmb` (Windows): `set_force_alt_for_test(true)` +
`SendMessage(WM_RBUTTONDOWN)` sincrono mismo-hilo. Asserta
`alt_rmb_resize_count` incrementa.
- `p6.alt_lmb` (Windows): mismo patron para `WM_LBUTTONDOWN`. Asserta
`alt_lmb_move_count` incrementa.
Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`.
Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh &&
e2e_run_cpp_windows altsnap_jitter_test`.
NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app
necesita renderizar incluso durante external move (caso raro: telemetria
@@ -261,3 +348,137 @@ de antes: `imgui.ini` es la unica fuente.
- App headless / capture mode: `cfg.auto_layouts = false`.
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
`local_files/`).
### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16
Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en
`<app_dir>/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro
`add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si
`WIN32` + existe `<CMAKE_CURRENT_SOURCE_DIR>/appicon.ico`, genera un
`<target>_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con
`IDI_ICON1 ICON "<path>"` y lo anade a `add_executable`. El compilador RC
(`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`)
lo enlaza al `.exe` como recurso `.rsrc`.
Verificar: `x86_64-w64-mingw32-objdump -h <app>.exe | grep rsrc` debe
mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara
`LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`).
#### Crear `.ico` para una app nueva
Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de
`https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`,
`bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor
legibilidad a 16/24px.
Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor
sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una
linea por app:
```python
from infra import generate_app_icon
generate_app_icon(
phosphor_icon_name="chart-bar",
accent_hex="#0ea5e9",
out_ico_path="apps/chart_demo/appicon.ico",
)
```
Mapping vive en el frontmatter de cada `app.md` C++:
```yaml
description: "Frase corta de 1 linea — que hace la app y por que existe."
icon:
phosphor: "chart-bar"
accent: "#0ea5e9"
```
### Trio obligatorio: description + icon.phosphor + icon.accent
**REGLA DURA:** TODA app C++/imgui declara los **3 campos JUNTOS** en su `app.md`:
1. `description:` (string corta, 1 linea) — texto que el `app_hub_launcher` muestra en la tarjeta y que el dashboard usa para tooltips.
2. `icon.phosphor:` (nombre del glyph Phosphor sin sufijo `-fill`) — glyph del icono.
3. `icon.accent:` (hex `#rrggbb`) — color del fondo redondeado del icono **Y** color del boton/border de la tarjeta en `app_hub_launcher`.
Los 3 se consumen como un set unico: el icono visual + el texto + el color de marca de la app. Una app sin descripcion aparece como tarjeta gris sin texto; sin `icon:` cae al default (`app-window` slate); sin accent el boton del hub aparece blanco. **Documentar uno sin los otros es bug**, no estilo.
### Refrescar el App Hub tras editar el trio
`app_hub_launcher` cachea iconos (PNG) y manifest (TSV) al arrancar. Cambiar `description`/`icon.*` en un `app.md` requiere regenerar ambos sidecars + relanzar el hub. Pipeline canonico:
```bash
./fn run refresh_app_hub # icons + manifest + restart hub
./fn run refresh_app_hub --no-restart # solo regenera, util si el hub esta cerrado
./fn run refresh_app_hub --size 128 # PNGs 128px en vez de 64
```
ID: `refresh_app_hub_bash_pipelines`. Compone `export_hub_icons_py_infra` + `export_hub_manifest_py_infra` + `is_cpp_app_running_windows_bash_infra` + `launch_cpp_app_windows_bash_infra`.
Regeneracion batch via pipeline del registry — escanea `app.md`s y compone
`generate_app_icon` por app. Anadir app nueva: declarar `icon:` en su
`app.md` y lanzar:
```bash
./fn run regenerate_app_icons # todas
./fn run regenerate_app_icons chart_demo # solo una
```
Convenciones:
- **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`).
- **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700
funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.).
- **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado.
- **Glyph color**: siempre blanco sobre el fondo accent.
Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/`
con `ls | grep <keyword>` antes de inventar — 1512 disponibles.
#### Re-deploy tras cambiar icono
```bash
# 1. Editar icon: en apps/chart_demo/app.md y regenerar
./fn run regenerate_app_icons chart_demo
# (o ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" para uno suelto sin tocar app.md)
# 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc)
./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build
```
Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras
desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer.
#### Runtime attach: taskbar + title bar + Alt+Tab (2026-05-16)
Embeber `.ico` en el `.exe` (windres) basta para File Explorer / shortcuts —
pero GLFW crea su WNDCLASS sin icono, asi que la **barra de tareas**, el
**header de la ventana** y **Alt+Tab** muestran el icono GLFW por defecto a
menos que adjuntemos el recurso al HWND en runtime.
`fn::run_app` lo hace automaticamente, sin opt-in. Tras `glfwCreateWindow`:
```cpp
HICON hSmall = LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(101),
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_SHARED);
HICON hBig = LoadImageW(..., SM_CXICON, SM_CYICON, LR_SHARED);
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); // title bar
SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); // taskbar
SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall);
SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig);
```
Resource ID `101` lo emite `add_imgui_app` en el `.rc` generado
(`101 ICON "<app_dir>/appicon.ico"`). Si la app no tiene `appicon.ico`, el
`.rc` no se genera, `LoadImageW` devuelve NULL y el HWND queda con el icono
GLFW por defecto (sin error).
Cobertura multi-viewport: el per-frame scan de `pio.Viewports` (mismo que
instala el sizemove subclass) tambien llama `attach_app_icon_to_hwnd` sobre
cada HWND secundario nuevo. Floating panels dragged-out heredan el icono
sin codigo extra en la app.
Cache shell: el pipeline `redeploy_cpp_app_windows` llama
`refresh_windows_icon_cache_bash_infra` tras copiar el .exe — invoca
`ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar
a que detecte el cambio por timestamp. Si Explorer sigue mostrando el
icono viejo: borrar `%LOCALAPPDATA%\IconCache.db` + reiniciar Explorer.
+12 -1
View File
@@ -20,10 +20,18 @@ fn doctor sync # Solo drift pc_locations BD vs disco local
fn doctor uses-functions # Solo audit imports reales vs uses_functions
fn doctor unused # Solo funciones huerfanas del registry
fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado)
# + check BeginTable inline: CANDIDATE (no migrado) / MIXED (parcial) / silencio (limpio)
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
```
`fn doctor cpp-apps` produce dos secciones:
1. Conformance (cfg.about/log, fn::run_app, menubar, DockSpace) — una fila por app imgui.
2. BeginTable migration (issue 0081) — solo apps con `ImGui::BeginTable` inline:
- `CANDIDATE`: N tablas inline sin `data_table_cpp_viz` en uses_functions. Considerar migracion.
- `MIXED`: N tablas inline con `data_table_cpp_viz` ya declarado. Migracion parcial OK.
- silencio: 0 BeginTable inline (limpio o completamente migrado).
### Mapeo subcomando → funcion del registry
| Subcomando | Funcion |
@@ -33,7 +41,8 @@ fn doctor --json # Salida JSON (cualquier subcomando) — para agentes
| `sync` | `pc_locations_drift_go_infra` |
| `uses-functions` | `audit_uses_functions_go_infra` |
| `unused` | `find_unused_functions_go_infra` |
| `cpp-apps` | `audit_cpp_apps_go_infra` |
| `cpp-apps` (conformance) | `audit_cpp_apps_go_infra` |
| `cpp-apps` (table migration) | `audit_cpp_table_migration_go_infra` (inline en `audit_cpp_apps.go`) |
Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente.
@@ -64,6 +73,8 @@ Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable
| `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio |
| `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` |
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
| cpp-apps BeginTable `CANDIDATE` | App tiene N `ImGui::BeginTable` sin migrar. Abrir rama TBD, reemplazar tablas por `data_table::render()` via `fn_table_viz`, añadir `data_table_cpp_viz` a `uses_functions` en `app.md` |
| cpp-apps BeginTable `MIXED` | Migracion parcial en curso. Continuar wave por wave hasta que no queden BeginTable inline |
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
### Para agentes
+23
View File
@@ -28,3 +28,26 @@ Documentar en el `app.md` del service:
- El puerto que usa (si expone HTTP/gRPC)
- Como lanzarlo y pararlo
- Como comprobar que esta vivo (health check)
### Bloque `service:` obligatorio (issue 0105)
Toda app con `tag: service` declara el bloque `service:` en su frontmatter. El indexer lo persiste en columnas dedicadas de `apps` + tabla `service_targets`. Consumido por `services_api`/`services_monitor` (issue 0106) y por `fn doctor services-spec`.
```yaml
service:
port: 8484 # null si no expone HTTP (stdio, daemon sin API)
health_endpoint: /api/databases # ruta GET, 2xx/3xx = sano; null si no aplica
health_timeout_s: 3
systemd_unit: sqlite_api.service # obligatorio si runtime empieza con `systemd-`
systemd_scope: user # user|system|null (docker-compose)
restart_policy: always # always|on-failure|none
runtime: systemd-user # systemd-user|systemd-system|docker-compose|stdio|manual
pc_targets: # >=1, pc_id de pc_locations
- aurgi-pc
- home-wsl
is_local_only: false # true => no se monitoriza por SSH (siempre local)
```
Validacion: `fn doctor services-spec` (`functions/infra/audit_services_spec.go`). Hoy 11/11 services con bloque completo.
**Gotcha critico:** usar `Restart=always` (no `on-failure`) en el unit systemd. Un `SIGTERM` limpio es exit success → `on-failure` NO reinicia y el service se queda muerto silenciosamente. `sqlite_api.service` cayo 20h asi el 2026-05-17.
+34 -2
View File
@@ -1,3 +1,35 @@
IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`).
## ids_naming — formato predictible
Nombres de funciones en snake_case. Tipos en PascalCase para Go.
IDs: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). Predictibilidad alta -> Claude descubre por fuzzy match sin lookup. Issue 0087.
### Reglas
1. **snake_case**: `[a-z0-9_]+`. Nada de PascalCase, kebab-case, dot.notation.
2. **Verbo obligatorio**: al menos un token del `name` debe ser un verbo de accion. El verbo puede ir delante (`get_user`) o detras (`user_lookup`). Ejemplos validos: `filter_slice`, `bank_login`, `metabase_get_dashboard`, `redeploy_cpp_app`. Invalidos: `slice` (sustantivo solo), `user` (sustantivo solo), `data` (sustantivo solo).
3. **Dominio canonico**: el `domain` debe estar en la lista canonica (ver `mcp__registry__fn_list_domains`). Crear dominio nuevo solo si el bucket es claramente distinto y se anade en el mismo turno a CLAUDE.md.
4. **Tipos en PascalCase Go**: `ResultGoCore`, `ErrorGoCore`. Aplica solo al codigo Go; el ID en el registry sigue siendo snake_case (`result_go_core`).
### Verbos canonicos (allowlist)
Lista no exhaustiva pero cubre la mayoria. Anadir aqui (y al validator en `apps/registry_mcp/naming.go`) cuando se introduzca un verbo nuevo recurrente.
`get, set, list, find, search, show, read, load, fetch, scan, query, lookup, parse, format, encode, decode, marshal, unmarshal, serialize, deserialize, validate, check, ensure, verify, audit, diagnose, test, match, filter, map, reduce, sort, group, count, sum, aggregate, compute, calculate, score, rank, cluster, classify, detect, init, create, make, build, generate, scaffold, install, setup, configure, register, add, insert, append, prepend, update, upsert, modify, edit, patch, replace, delete, remove, clear, drop, prune, clean, copy, move, rename, sync, clone, extract, inject, import, export, send, post, put, call, dispatch, exec, run, launch, start, stop, kill, restart, redeploy, deploy, open, close, connect, disconnect, login, logout, authenticate, enable, disable, toggle, lock, unlock, propose, promote, deprecate, approve, reject, emit, render, draw, paint, serve, host, pull, push, checkout, commit, tag, merge, rebase, watch, monitor, observe, log, trace, profile, benchmark, snapshot, backup, restore, archive, compress, decompress, hash, encrypt, decrypt, sign, taskkill, recopile, vault, propose, apply, gather, collect, fold, head, tail, take, drop, slice, chunk, batch, debounce, throttle, retry, await, sleep, ping, kill, prime, warm, refresh, invalidate, reload, reset, rollback, fork, spawn, daemon, observe, plot, draw, capture, replay, recopilate`
### Excepciones
- **Operadores matematicos/estadisticos** ampliamente reconocidos por acronimo: `sma`, `ema`, `rsi`, `vwap`, `adx`. Validator hace allowlist explicita.
- **Tipos** (entity_type `type`): no requieren verbo. Validator lo salta cuando `kind=type`.
- **Components** (`kind: component`): nombre describe artefacto UI (`button_primary`, `chat_panel`). Permite forma `<noun>_<modifier>`. Validator salta el check de verbo si `kind=component`.
### Validator
`mcp__registry__fn_create_function` ejecuta el validator antes de escribir archivos. Rechaza con error si:
- name no es snake_case.
- name no contiene verbo (excepto component/type).
- domain no esta en lista canonica.
Error tipico:
```
naming: name "slice" lacks action verb. Add verb prefix/suffix (e.g. filter_slice, slice_window). See .claude/rules/ids_naming.md.
naming: domain "bizops" not in canonical list (core, infra, finance, ...). Add it to CLAUDE.md and rules first.
```
+52
View File
@@ -0,0 +1,52 @@
## Slash commands por project (namespaced)
Cada `projects/<p>/` puede tener su propio `.claude/commands/*.md`. Para invocarlos desde la raiz de `fn_registry` sin que pisen los comandos globales, se exponen via **symlink namespaced** en `fn_registry/.claude/commands/<project>/`.
### Patron canonico
```
projects/aurgi/.claude/commands/foo.md # archivo real (viaja con el sub-repo del project)
fn_registry/.claude/commands/aurgi -> symlink -> ../../projects/aurgi/.claude/commands
```
Resultado:
| cwd | Invocacion |
|---|---|
| `cd projects/aurgi && claude` | `/foo` (sin namespace) |
| `cd fn_registry && claude` | `/aurgi:foo` (namespaced, no colisiona con `/foo` global) |
Subdirs dentro de `.claude/commands/` se exponen como namespace en el slash command. Por eso `aurgi/foo.md` -> `/aurgi:foo`.
### Como anadir un project nuevo
1. `mkdir -p projects/<p>/.claude/commands/`.
2. Crear `<comando>.md` con frontmatter `description:` + cuerpo.
3. Symlink: `ln -sf ../../projects/<p>/.claude/commands /home/egutierrez/fn_registry/.claude/commands/<p>`.
4. Versionar el `.claude/commands/` del project en su propio sub-repo (NO en fn_registry — projects estan gitignored).
5. Versionar SOLO el symlink en fn_registry (`git add .claude/commands/<p>`).
### Reglas
- Cada project mantiene autonomia: sus commands viajan con el sub-repo y funcionan tanto en `cd projects/<p>` como desde la raiz.
- El symlink en fn_registry da acceso global con namespace — sin colision con commands del registry.
- NO duplicar contenido: archivo real solo en `projects/<p>/.claude/commands/`. fn_registry solo guarda el symlink.
- Si el project se mueve/elimina, borrar el symlink en fn_registry.
### Listado actual
| Project | Symlink | Commands disponibles desde fn_registry |
|---|---|---|
| aurgi | `.claude/commands/aurgi` | `/aurgi:aumentar_task`, `/aurgi:contexto_aurgi`, `/aurgi:anadir_contexto_aurgi` |
Anadir filas aqui al introducir un project nuevo con commands.
### Catalogo dinamico
Para listado en tiempo real (sin tener que actualizar esta tabla a mano): `/commands` escanea `.claude/commands/` recursivo y agrupa por namespace. Filtros: `/commands <substring>`, `/commands --ns <ns>`, `/commands --json`.
### Gotchas
- Claude Code lista los commands disponibles al inicio de sesion. Si un symlink apunta a un directorio inexistente, los commands no aparecen — verificar con `ls -L .claude/commands/<project>/`.
- El namespace usa el nombre del subdirectorio (`aurgi/`), no del project en `projects/`. Mantenerlos iguales para evitar confusion.
- Los commands del project se ejecutan con el cwd de la sesion actual. Un `/aurgi:aumentar_task` invocado desde `fn_registry/` corre con cwd `fn_registry/` — paths relativos en el `.md` deben asumir esto (siempre usar paths relativos al repo, ej. `projects/aurgi/vaults/...`).
+1 -1
View File
@@ -140,7 +140,7 @@ Cobertura por capa, no todas activas a la vez:
### Que NO se monitoriza
- Funcion Go/C++ llamada internamente por app ya compilada.
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
- Funcion ejecutada por systemd timer / cron / dag_engine **step `command:`** (no `function:`) sin pasar por `fn run`. Nota: dag_engine steps con `function:` SI quedan trazados — el executor invoca `fn run <id>` y guarda `function_id` en `dag_step_results`.
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
- Service de produccion recibiendo HTTP.
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Append a one-liner [[fn_id]] — purpose to MEMORY.md after fn-constructor
# creates a new registry function. Idempotent: skips if id already present.
# Used by /fn_claude step 5b (issue 0087, pieza 6).
#
# Usage: append_fn_to_memory.sh <fn_id> "<one-line purpose>"
set -euo pipefail
FN_ID="${1:-}"
PURPOSE="${2:-}"
if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then
echo "usage: append_fn_to_memory.sh <fn_id> <purpose>" >&2
exit 2
fi
MEM_DIR="${CLAUDE_MEMORY_DIR:-/home/lucas/.claude/projects/-home-lucas-fn-registry/memory}"
MEM_FILE="$MEM_DIR/MEMORY.md"
[ -d "$MEM_DIR" ] || { echo "memory dir missing: $MEM_DIR" >&2; exit 1; }
[ -f "$MEM_FILE" ] || { echo "MEMORY.md missing: $MEM_FILE" >&2; exit 1; }
# Per-function reference file slug
SLUG="reference_fn_${FN_ID}.md"
REF_FILE="$MEM_DIR/$SLUG"
# Idempotency: if already linked in MEMORY.md, exit 0
if grep -qF "[fn-$FN_ID]" "$MEM_FILE" 2>/dev/null; then
echo "already in MEMORY.md: $FN_ID"
exit 0
fi
# 1. Create reference memory file
cat > "$REF_FILE" <<EOF
---
name: fn-$FN_ID
description: Registry function $FN_ID — $PURPOSE
metadata:
type: reference
---
Registry function: \`$FN_ID\`
$PURPOSE
Invoke via \`./fn run $FN_ID [args]\` or \`mcp__registry__fn_run id="$FN_ID"\`. Inspect with \`mcp__registry__fn_show id="$FN_ID"\` / \`mcp__registry__fn_code id="$FN_ID"\`.
EOF
# 2. Append index line to MEMORY.md
printf -- '- [%s](%s) — %s\n' "fn-$FN_ID" "$SLUG" "$PURPOSE" >> "$MEM_FILE"
echo "appended: $FN_ID -> $MEM_FILE"
@@ -0,0 +1,121 @@
#!/bin/bash
# integrate-worktrees.sh — Integra branches de worktrees a master con --no-ff
#
# Uso: ./integrate-worktrees.sh <slug-1> <slug-2> ...
# Ejemplo: ./integrate-worktrees.sh 0026-split-runtime 0027-prune-config-schema
#
# Para cada slug:
# 1. git merge --no-ff issue/<slug> a master
# 2. Verificar que master compila después del merge
# 3. Si hay conflict o fallo de build, PARAR inmediatamente
#
# Los slugs deben pasarse en el orden correcto (waves ya resueltas).
# NO hace push — eso lo decide el usuario.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
if [ $# -eq 0 ]; then
echo "ERROR: se necesita al menos un slug"
echo "Uso: $0 <slug-1> <slug-2> ..."
exit 1
fi
# Asegurar que estamos en master
echo "=== Cambiando a master ==="
cd "$REPO_ROOT"
git checkout master
MERGED=0
FAILED_AT=""
for slug in "$@"; do
branch="issue/${slug}"
echo ""
echo "=== Integrando: ${branch} ==="
# Verificar que la branch existe
if ! git show-ref --verify --quiet "refs/heads/${branch}"; then
echo "FAIL: branch ${branch} no existe"
FAILED_AT="$slug"
break
fi
# Merge --no-ff
if ! git merge --no-ff "$branch" -m "merge: ${branch} — implementación paralela"; then
echo ""
echo "CONFLICT: merge de ${branch} tiene conflictos"
echo "Resolver manualmente y luego continuar con los slugs restantes"
echo ""
echo "Para resolver:"
echo " 1. git status (ver archivos en conflicto)"
echo " 2. Resolver conflictos en cada archivo"
echo " 3. git add <archivos>"
echo " 4. git commit"
echo ""
echo "Slugs pendientes después de ${slug}:"
FOUND=0
for remaining in "$@"; do
if [ "$FOUND" -eq 1 ]; then
echo " - ${remaining}"
fi
if [ "$remaining" = "$slug" ]; then
FOUND=1
fi
done
exit 1
fi
echo "MERGED: ${branch}"
# Verificar que master sigue compilando (si BUILD_CMD esta definido)
if [ -n "${BUILD_CMD:-}" ]; then
echo "--- Verificando build post-merge ($BUILD_CMD) ---"
if ! (cd "$REPO_ROOT" && bash -c "$BUILD_CMD" 2>&1); then
echo ""
echo "FAIL: master no compila despues de mergear ${branch}"
echo "Revertir con: git reset --hard HEAD~1"
echo "Investigar el problema antes de continuar."
FAILED_AT="$slug"
break
fi
echo "OK: build post-merge exitoso"
else
echo "--- Build post-merge SKIPPED (BUILD_CMD no definido) ---"
fi
MERGED=$((MERGED + 1))
done
echo ""
echo "=== Resumen de integración ==="
echo "Mergeados: ${MERGED} de $#"
if [ -n "$FAILED_AT" ]; then
echo "Falló en: ${FAILED_AT}"
echo ""
echo "Worktrees NO limpiados (resolver primero el fallo)"
exit 1
fi
# Limpieza de worktrees y branches
echo ""
echo "=== Limpieza ==="
for slug in "$@"; do
path="${REPO_ROOT}/worktrees/${slug}"
branch="issue/${slug}"
if [ -d "$path" ]; then
git worktree remove "$path" 2>/dev/null && echo "REMOVED: worktree ${path}" || echo "WARN: no se pudo eliminar worktree ${path}"
fi
git branch -d "$branch" 2>/dev/null && echo "DELETED: branch ${branch}" || echo "WARN: no se pudo eliminar branch ${branch}"
done
echo ""
echo "=== Integración completa ==="
echo "Master tiene ${MERGED} merges nuevos."
echo ""
echo "Para publicar: git push"
@@ -0,0 +1,74 @@
#!/bin/bash
# setup-worktrees.sh — Crea git worktrees para ejecución paralela de issues
#
# Uso: ./setup-worktrees.sh <slug-1> <slug-2> ...
# Ejemplo: ./setup-worktrees.sh 0026-split-runtime 0027-prune-config-schema
#
# Cada slug genera:
# worktrees/<slug>/ (worktree completo)
# branch: issue/<slug>
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE_DIR="${REPO_ROOT}/worktrees"
if [ $# -eq 0 ]; then
echo "ERROR: se necesita al menos un slug de issue"
echo "Uso: $0 <slug-1> <slug-2> ..."
exit 1
fi
# Verificar master (NO pull --rebase: rompe merges locales convirtiendolos
# en cherry-picks contra origin/master viejo). Detectado 2026-05-18.
echo "=== Verificando master ==="
CURRENT_BRANCH="$(git branch --show-current)"
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
echo "WARN: estas en branch '${CURRENT_BRANCH}', no master. Worktrees nuevos saldran de master ref de todos modos."
fi
# NO auto-pull. Usuario decide sync con remote.
mkdir -p "$WORKTREE_DIR"
CREATED=0
SKIPPED=0
FAILED=0
for slug in "$@"; do
branch="issue/${slug}"
path="${WORKTREE_DIR}/${slug}"
if [ -d "$path" ]; then
echo "SKIP: worktree ya existe: ${path}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Verificar que la branch no existe ya
if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
echo "WARN: branch ${branch} ya existe, creando worktree desde ella"
git worktree add "$path" "$branch" 2>/dev/null || {
echo "FAIL: no se pudo crear worktree para ${slug}"
FAILED=$((FAILED + 1))
continue
}
else
echo "CREATE: worktree ${path} (branch ${branch})"
git worktree add -b "$branch" "$path" master 2>/dev/null || {
echo "FAIL: no se pudo crear worktree para ${slug}"
FAILED=$((FAILED + 1))
continue
}
fi
CREATED=$((CREATED + 1))
done
echo ""
echo "=== Resumen ==="
echo "Creados: ${CREATED}"
echo "Existentes: ${SKIPPED}"
echo "Fallidos: ${FAILED}"
echo ""
echo "=== Worktrees activos ==="
git worktree list
@@ -0,0 +1,165 @@
#!/bin/bash
# verify-worktree.sh — Verifica build, tests y cierre de issue en un worktree.
#
# Uso:
# ./verify-worktree.sh <worktree-path> [build-cmd] [test-cmd]
#
# Ejemplos:
# ./verify-worktree.sh worktrees/0026-foo
# ./verify-worktree.sh worktrees/0026-foo "go build -tags fts5 ./..." "go test -tags fts5 ./..."
# BUILD_CMD="cmake --build cpp/build" TEST_CMD="ctest --test-dir cpp/build" ./verify-worktree.sh worktrees/0026-foo
#
# Resolucion de comandos (en orden de prioridad):
# 1. Argumentos posicionales (build-cmd, test-cmd)
# 2. Variables de entorno BUILD_CMD / TEST_CMD
# 3. Archivo .parallel-fix-issues.yml en la raiz del worktree (claves: build, test)
# 4. Auto-deteccion segun ficheros del proyecto:
# - go.mod → "go build ./..." + "go test ./..."
# - CMakeLists.txt → "cmake -S . -B build && cmake --build build" + "ctest --test-dir build"
# - Cargo.toml → "cargo build" + "cargo test"
# - package.json → "npm run build" + "npm test"
# - pyproject.toml → "" + "pytest"
# 5. Si nada se detecta, salta build/test con WARN.
#
# Auto-deteccion adicional: si hay go.mod, intenta extraer build tag de //go:build.
#
# Exit codes:
# 0 = todo OK
# 1 = error de argumento
# 2 = build fallo
# 3 = tests fallaron
# 4 = issue no cerrado (solo WARN, no falla)
# 5 = sin commits propios
set -euo pipefail
if [ $# -lt 1 ]; then
echo "ERROR: se necesita el path del worktree"
echo "Uso: $0 <worktree-path> [build-cmd] [test-cmd]"
exit 1
fi
WORKTREE="$1"
ARG_BUILD_CMD="${2:-}"
ARG_TEST_CMD="${3:-}"
# Resolver path absoluto
if [[ "$WORKTREE" != /* ]]; then
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE="${REPO_ROOT}/${WORKTREE}"
fi
if [ ! -d "$WORKTREE" ]; then
echo "ERROR: worktree no encontrado: ${WORKTREE}"
exit 1
fi
SLUG="$(basename "$WORKTREE")"
echo "=== Verificando: ${SLUG} ==="
# --- Resolver build/test commands ---
BUILD_CMD="${ARG_BUILD_CMD:-${BUILD_CMD:-}}"
TEST_CMD="${ARG_TEST_CMD:-${TEST_CMD:-}}"
# Manifest opcional
MANIFEST="${WORKTREE}/.parallel-fix-issues.yml"
if [ -z "$BUILD_CMD" ] && [ -f "$MANIFEST" ]; then
M_BUILD=$(grep -E "^build:" "$MANIFEST" 2>/dev/null | sed -E 's/^build:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
if [ -n "$M_BUILD" ]; then BUILD_CMD="$M_BUILD"; echo "INFO: build desde manifest"; fi
fi
if [ -z "$TEST_CMD" ] && [ -f "$MANIFEST" ]; then
M_TEST=$(grep -E "^test:" "$MANIFEST" 2>/dev/null | sed -E 's/^test:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
if [ -n "$M_TEST" ]; then TEST_CMD="$M_TEST"; echo "INFO: test desde manifest"; fi
fi
# Auto-deteccion
if [ -z "$BUILD_CMD" ] || [ -z "$TEST_CMD" ]; then
AUTO_BUILD=""
AUTO_TEST=""
if [ -f "${WORKTREE}/go.mod" ]; then
# Detectar build tag
AUTO_TAG=$(grep -rh "^//go:build " --include="*.go" "$WORKTREE" 2>/dev/null \
| sed -E 's|^//go:build ([a-zA-Z0-9_]+).*|\1|' \
| sort -u | head -1 || true)
TAG_FLAG=""
[ -n "$AUTO_TAG" ] && TAG_FLAG="-tags $AUTO_TAG"
AUTO_BUILD="go build $TAG_FLAG ./..."
AUTO_TEST="go test $TAG_FLAG ./..."
echo "INFO: stack detectado: Go${TAG_FLAG:+ ($TAG_FLAG)}"
elif [ -f "${WORKTREE}/CMakeLists.txt" ] || ls "${WORKTREE}"/cpp/CMakeLists.txt >/dev/null 2>&1; then
CMAKE_DIR="."
[ -f "${WORKTREE}/cpp/CMakeLists.txt" ] && [ ! -f "${WORKTREE}/CMakeLists.txt" ] && CMAKE_DIR="cpp"
AUTO_BUILD="cmake -S ${CMAKE_DIR} -B ${CMAKE_DIR}/build -DCMAKE_BUILD_TYPE=Release && cmake --build ${CMAKE_DIR}/build -j"
AUTO_TEST="ctest --test-dir ${CMAKE_DIR}/build --output-on-failure || true"
echo "INFO: stack detectado: C++/CMake (dir=${CMAKE_DIR})"
elif [ -f "${WORKTREE}/Cargo.toml" ]; then
AUTO_BUILD="cargo build"
AUTO_TEST="cargo test"
echo "INFO: stack detectado: Rust"
elif [ -f "${WORKTREE}/package.json" ]; then
AUTO_BUILD="npm run build --if-present"
AUTO_TEST="npm test --if-present"
echo "INFO: stack detectado: Node"
elif [ -f "${WORKTREE}/pyproject.toml" ] || [ -f "${WORKTREE}/setup.py" ]; then
AUTO_BUILD="" # python normalmente no tiene build step
AUTO_TEST="pytest"
echo "INFO: stack detectado: Python"
else
echo "WARN: no se detecto stack; usar BUILD_CMD/TEST_CMD env o manifest .parallel-fix-issues.yml"
fi
[ -z "$BUILD_CMD" ] && BUILD_CMD="$AUTO_BUILD"
[ -z "$TEST_CMD" ] && TEST_CMD="$AUTO_TEST"
fi
# 1. Verificar commits propios
echo ""
echo "--- Commits propios ---"
COMMIT_COUNT=$(cd "$WORKTREE" && git log master..HEAD --oneline 2>/dev/null | wc -l)
if [ "$COMMIT_COUNT" -eq 0 ]; then
echo "FAIL: sin commits propios en la branch"
exit 5
fi
echo "OK: ${COMMIT_COUNT} commits desde master"
cd "$WORKTREE" && git log master..HEAD --oneline
# 2. Build
echo ""
if [ -n "$BUILD_CMD" ]; then
echo "--- Build ($BUILD_CMD) ---"
if (cd "$WORKTREE" && bash -c "$BUILD_CMD" 2>&1); then
echo "OK: build exitoso"
else
echo "FAIL: build fallo"
exit 2
fi
else
echo "--- Build SKIPPED (sin comando) ---"
fi
# 3. Tests
echo ""
if [ -n "$TEST_CMD" ]; then
echo "--- Tests ($TEST_CMD) ---"
if (cd "$WORKTREE" && bash -c "$TEST_CMD" 2>&1); then
echo "OK: tests pasaron"
else
echo "FAIL: tests fallaron"
exit 3
fi
else
echo "--- Tests SKIPPED (sin comando) ---"
fi
# 4. Issue cerrado
echo ""
echo "--- Cierre de issue ---"
COMPLETED_FILES=$(cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/ 2>/dev/null | wc -l)
if [ "$COMPLETED_FILES" -gt 0 ]; then
echo "OK: issue movido a completed/"
cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/
else
echo "WARN: no se detecto issue movido a completed/ (verificar manualmente)"
fi
echo ""
echo "=== RESULTADO: ${SLUG} — OK ==="
+7
View File
@@ -81,3 +81,10 @@ broken_paths.txt
imgui.ini
prompts/
kotlin/functions/ui/
# Module versioning auto-generated headers (written by `fn index`, issue 0097)
**/version_generated.h
**/app_modules_generated.h
# Issue migration backups (0100)
dev/issues/.backup_pre_*
+62
View File
@@ -8,6 +8,68 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
## [Unreleased]
## 2026-05-17
### Added
- **Bloque `service:` en frontmatter de `app.md`** (issue 0105) — toda app con `tag: service` declara ahora `port`, `health_endpoint`, `health_timeout_s`, `systemd_unit`, `systemd_scope`, `restart_policy`, `runtime` (`systemd-user|systemd-system|docker-compose|stdio|manual`), `pc_targets[]`, `is_local_only`. 11 apps actualizadas: `sqlite_api`, `dag_engine`, `call_monitor`, `kanban`, `deploy_server`, `registry_mcp`, `registry_api`, `footprint_geo_stack`, `element_matrix_chat`, `agents_and_robots`, `services_api`.
- **Migration `014_service_metadata.sql`** — anade 8 columnas (`service_port`, `service_health_endpoint`, `service_health_timeout_s`, `service_systemd_unit`, `service_systemd_scope`, `service_restart_policy`, `service_runtime`, `service_is_local_only`) a `apps` + tabla nueva `service_targets (app_id, pc_id, role)` con indices por `app_id` y `pc_id`.
- **`registry.App.Service *ServiceSpec`** + parser `rawService` + escritura/lectura en `InsertApp`/`scanApps`/`Purge` (preserva `service_targets`). API publica `db.GetServicePCTargets(appID) []string`.
- **`audit_services_spec_go_infra`** (`functions/infra/audit_services_spec.{go,md}`) — audita apps `tag: service` y reporta drift del bloque `service:` (runtime allowlist, pc_targets >=1, systemd_unit obligatorio si `runtime` empieza con `systemd-`, restart_policy en `always|on-failure|none`).
- **`fn doctor services-spec`** — subcomando nuevo en `cmd/fn/doctor.go`. Salida tabwriter + `--json`. Hoy: `11/11 services with complete service: block`.
- **App `services_api`** (`apps/services_api/`, issue 0106) — Go HTTP daemon en `127.0.0.1:8485`. Loop paralelo cada 15s (max 8 in-flight, timeout 20s/probe) que reconcilia esperado vs real para cada `(app, pc)` cruzado de `service_targets`. Probes locales (`systemctl is-active` + TCP dial + `http.Client`) o remotos (`ssh_exec_go_infra`). Persiste en `operations.db`: `service_state` (snapshot actual) + `service_transition` (cambios de overall append-only). Endpoints `GET /api/health`, `GET /api/services`, `POST /api/check`, `GET /api/pcs`. systemd unit `~/.config/systemd/user/services_api.service` con `Restart=always`.
- **App `services_monitor`** (`apps/services_monitor/`, issue 0106) — frontend C++ ImGui. Polling auto cada 5s configurable + boton "Force check" (POST `/api/check`). Tabla 9-col agrupada por app: overall pill, systemd state, port + listening flag (`TI_PLUG`/`TI_PLUG_CONNECTED`), HTTP status+latency, runtime, last change age, error/note. JSON via `vendor/nlohmann/json.hpp` (copiado de data_factory). HTTP socket TCP via `http_client.{cpp,h}` (copiado de data_factory). Build linux + windows con `add_imgui_app` + ws2_32 en Win. Deploy automatico via `redeploy_cpp_app_windows`.
- **Issues 0105 + 0106** (`dev/issues/`) — estandarizacion del bloque `service:` y app `services_monitor`.
### Fixed
- **`sqlite_api.service` murio 20h sin alerta el 2026-05-17** — Raiz: el unit tenia `Restart=on-failure` y el ultimo exit fue por `SIGTERM` (limpio, no failure). systemd NO reinicia exit success. Fix: cambio a `Restart=always` + `RestartSec=5`. Reload + restart inmediato. Detectado mientras se debuggeaba `data_factory` cargando lento (raiz: data_factory llama a `sqlite_api:8484`, timeout 3s, no responde). Aplicado el mismo `Restart=always` al unit nuevo `services_api.service`.
- **`sqlite_api/app.md` health_endpoint** — declaraba `/api/status` que devuelve 404. Cambiado a `/api/databases` (200, lista de bases registradas). Detectado por el primer ciclo del propio `services_api` que marcaba sqlite_api como `degraded`.
### Changed
- **`services_monitor` tags** — sin `service`/`services` en `tags` para evitar falso positivo en el matcher `tags LIKE '%service%'` del audit `services-spec`. La app es desktop client (frontend), no daemon.
## 2026-05-16
### Added
- **Panel "Logs" en `dag_engine` RunDetail** — `apps/dag_engine/frontend/src/pages/RunDetail.tsx` anade `<Paper>` final con `<Code block>` scrollable + `CopyButton` de Mantine. Helper `buildLogText(run, steps)` compone texto plano (metadata del run + por-step status/exit/duration/stdout/stderr indentado) para pegar entero al LLM sin abrir los `Collapse` del `StepTimeline`.
### Fixed
- **`dag_engine` steps `function:` fallando con `error: function "<id>" not found (tried as ID and name)`** — tres DAGs nocturnos (`fn_backup` x2, `daily-registry-audit`) fallaron 2026-05-15/16 porque el binario `fn` resolvia una copia stale `apps/dag_engine/registry.db` (May 15, 262 KB) en vez del `registry.db` raiz. Raiz: el systemd unit `dag_engine.service` tiene `WorkingDirectory=apps/dag_engine/` y no exportaba `FN_REGISTRY_ROOT`; `cmd/fn/ops.go::tryOpenRegistryDB` cae al walk-up `go.mod` (devuelve `apps/dag_engine/`). Fix:
- Borrado `apps/dag_engine/registry.db` stale (violaba `.claude/rules/db_locations.md`).
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT`, `FN_BIN`, `PATH` (con `/usr/local/go/bin` para steps `function:` Go sin tests que invocan `go vet`), `HOME`.
- `apps/dag_engine/executor.go`: steps `function:` exportan `FN_REGISTRY_ROOT=<root>` en env y default `dir = fnRegistryRoot` si `step.Dir`/`dag.WorkingDir` vacios. Steps `command:`/`script:` sin cambio.
### Added
- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `<exe_dir>` desplegado.
- Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop.
- Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256).
- Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `<app_dir>/appicon.ico` y genera `<target>_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`).
- Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`.
- Convencion documentada en `.claude/rules/cpp_apps.md §11`.
- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame).
- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map<HWND, WNDPROC>`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND).
- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes.
- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal.
- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows.
- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`.
### Changed
- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui).
- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps/<X>/` (canonical issue 0096) ademas de `cpp/apps/<X>/` (legacy) y `projects/*/apps/<X>/`. Fix retroactivo: `./fn run compile_cpp_app <name>` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`.
### Notes
- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos.
- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`).
- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion.
## 2026-05-14
### Added
+4
View File
@@ -0,0 +1,4 @@
[2026-05-15 23:51:43.764] [INFO] app start: altsnap_jitter_test
[2026-05-15 23:51:44.017] [INFO] app exit
[2026-05-15 23:52:47.933] [INFO] app start: altsnap_jitter_test
[2026-05-15 23:52:48.135] [INFO] app exit
+360
View File
@@ -0,0 +1,360 @@
# dag_engine — Guia de uso
Motor de DAGs propio del fn_registry. **Scheduler oficial** del ecosistema (issue 0007a-e + flow 0001). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**.
---
## 1. Donde viven los DAGs
| Path | Que |
|---|---|
| `apps/dag_engine/dags_migrated/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). |
| `apps/dag_engine/dags_migrated/archive/` | DAGs deshabilitados (no se cargan por el scheduler). |
Por defecto el systemd unit apunta a `apps/dag_engine/dags_migrated/`. Para usar otro dir, edita `~/.config/systemd/user/dag_engine.service`:
```ini
ExecStart=/home/lucas/fn_registry/apps/dag_engine/dag_engine server \
--port 8090 \
--dags-dir /home/lucas/fn_registry/apps/dag_engine/dags_migrated \
--db /home/lucas/fn_registry/apps/dag_engine/dag_engine.db \
--scheduler
```
Y reload + restart:
```bash
systemctl --user daemon-reload
systemctl --user restart dag_engine.service
```
---
## 2. Anadir un DAG nuevo (workflow)
### Paso a paso
1. **Crear YAML** en `apps/dag_engine/dags_migrated/<nombre>.yaml` (ver formato en seccion 3).
2. **Validar** sin ejecutar:
```bash
./apps/dag_engine/dag_engine validate apps/dag_engine/dags_migrated/<nombre>.yaml
```
Salida esperada: `Validation: PASS`. Si falla, ver seccion 5 (diagnostico).
3. **Probar ejecucion manual** una vez:
```bash
./apps/dag_engine/dag_engine run apps/dag_engine/dags_migrated/<nombre>.yaml
```
4. **Recargar scheduler** (toma el YAML automaticamente al iterar el dir):
```bash
systemctl --user restart dag_engine.service
journalctl --user-unit dag_engine.service -n 30 --no-pager
```
Busca la linea `[scheduler] ticker started for <nombre> (<cron>)` en los logs.
5. **Verificar en frontend**:
- C++ ImGui: panel `DAGs` muestra el nuevo DAG. Pulsa `Refresh` si no aparece.
- Web: `http://localhost:8090`.
### Disparo manual desde curl o frontend
```bash
curl -X POST http://127.0.0.1:8090/api/dags/<nombre>/run
```
Devuelve `{"dag":"<nombre>","run_id":"...","status":"accepted"}` y dispara el WS broadcast — los frontends ven la run en `<1s`.
---
## 3. Formato YAML
Formato YAML propio de dag_engine. Schema: `name`, `description`, `schedule`, `env`, `tags`, `working_dir`, `steps[]`, `handlers` (alias `handler_on`).
### Ejemplo completo
```yaml
name: my_pipeline
description: "Pipeline diario que importa CSV y actualiza Metabase."
group: finanzas # opcional, agrupa DAGs en listados
type: graph # opcional: graph (default) | chain
tags: [daily, csv, metabase] # opcional, filtros en la UI
# Variables de entorno (heredadas por todos los steps).
env:
- DATA_DIR: /home/lucas/data
- SLACK_HOOK: ${SLACK_HOOK_PROD} # interpolacion de ENV del host
# Cron schedule. Puede ser string o lista.
schedule:
- "0 9 * * *" # 09:00 todos los dias
- "0 21 * * 5" # 21:00 viernes (segundo trigger)
# Working dir + shell por defecto para todos los steps.
working_dir: /home/lucas/fn_registry
shell: /bin/bash
timeout_sec: 1800 # 30 min para todo el DAG
steps:
- name: ingest
description: "Descarga CSV."
command: ./bash/functions/pipelines/ingest_csv.sh
timeout_sec: 300 # 5 min para este step
env:
- SOURCE_URL: https://example.com/data.csv
- name: transform
description: "Limpieza y agregacion."
script: |
#!/usr/bin/env python3
import pandas as pd
df = pd.read_csv("$DATA_DIR/raw.csv")
df.to_parquet("$DATA_DIR/clean.parquet")
depends: [ingest] # debe terminar OK antes
retry_policy:
limit: 2 # reintentos en caso de fallo
interval_sec: 60
- name: load_metabase
command: ./bash/functions/metabase/refresh_dashboard.sh
depends: [transform]
continue_on:
failure: true # no aborta el DAG aunque falle
- name: notify
command: ./bash/functions/io/slack_send.sh "pipeline OK"
depends: [load_metabase]
# Hooks de ciclo de vida.
handler_on:
success: ./bash/functions/io/notify_success.sh
failure: ./bash/functions/io/notify_failure.sh
exit: ./bash/functions/io/cleanup.sh
```
### Campos del DAG (top-level)
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador unico. Debe matchear el filename sin extension. |
| `description` | string | "" | Texto libre, aparece en la UI. |
| `group` | string | "" | Agrupa DAGs en listados. |
| `type` | string | `""` (graph) | `graph` o `chain`. graph = grafo dirigido por `depends`. chain = ejecucion secuencial implicita. |
| `working_dir` | string | cwd del server | Path absoluto desde donde lanzar los steps. |
| `shell` | string | `/bin/sh` | Shell para `command:`. |
| `env` | list/map | [] | Variables de entorno DAG-wide. |
| `schedule` | string/list | "" | Cron expressions (5 campos: min hour dom mon dow). Vacio = solo manual. |
| `steps` | list | (obligatorio) | Pasos del DAG (>=1). |
| `handler_on` | map | null | Hooks `init/success/failure/exit`. Alias: `handlers`. |
| `tags` | list[string] | [] | Filtros en la UI. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout global del DAG en segundos. |
### Campos de cada step
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador del step dentro del DAG. |
| `id` | string | "" | Override del id auto-generado. |
| `description` | string | "" | Texto libre. |
| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). |
| `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. |
| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run <id> <args...>` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. |
| `args` | list[string] | [] | Args extra para `command` o para la `function`. |
| `shell` | string | hereda | Override del shell. |
| `dir` / `working_dir` | string | hereda | Working dir para este step. |
| `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. |
| `env` | list/map | hereda | Env del step (sobrescribe el del DAG). |
| `continue_on.failure` | bool | false | Si true, el DAG sigue aunque este step falle. |
| `continue_on.skipped` | bool | false | Si true, dependientes corren aunque este step quede skipped. |
| `retry_policy.limit` | int | 0 | Reintentos. |
| `retry_policy.interval_sec` | int | 0 | Segundos entre reintentos. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout del step. |
| `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). |
| `tags` | list[string] | [] | Tags por step (UI). |
### Function steps (coherencia con el registry)
Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end.
```yaml
steps:
- name: audit_capabilities
function: audit_capability_groups_go_infra
args: ["--json"]
description: "Audita drift entre tags de capability group y paginas madre"
```
Ventajas vs `command: ./fn run ...`:
- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable).
- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente).
- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step.
- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error.
- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`.
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
Ejemplo completo: `apps/dag_engine/dags_migrated/daily-registry-audit.yaml`.
### Cron schedule
5 campos clasicos: `min hour dom mon dow`. Ejemplos:
| Expresion | Significado |
|---|---|
| `0 9 * * *` | Todos los dias a las 09:00 |
| `*/15 * * * *` | Cada 15 minutos |
| `0 */6 * * *` | Cada 6 horas en punto |
| `0 9 * * 1-5` | 09:00 lunes-viernes |
| `0 21 * * 5` | 21:00 viernes |
Multiples cron en `schedule:` -> el DAG dispara por cada uno.
---
## 4. Comandos CLI
```bash
./dag_engine run <path.yaml> # ejecuta un DAG ad-hoc
./dag_engine list [dir] # lista DAGs con schedule + ultimo status
./dag_engine status [dag_name] # historial de ejecuciones
./dag_engine validate <path.yaml> # parse + validate (no ejecuta)
./dag_engine server # arranca HTTP + WS hub + frontend embebido
```
Flags del `server`:
| Flag | Default | Que |
|---|---|---|
| `--port` | 8090 | Puerto HTTP. |
| `--dags-dir` | `apps/dag_engine/dags_migrated` (via systemd unit) | Dir scaneado para YAMLs. |
| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. |
| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. |
---
## 5. Que hacer si algo falla
### 5.1. El DAG no aparece en la UI
**Sintoma:** anadiste un YAML pero `GET /api/dags` no lo lista.
| Causa | Diagnostico | Fix |
|---|---|---|
| YAML invalido | `./dag_engine validate <path>` muestra el error. | Corregir segun el mensaje (campo desconocido, indentacion, type wrong). |
| Filename con extension fuera de `.yaml`/`.yml` | `ls apps/dag_engine/dags_migrated/` | Renombrar a `.yaml`. |
| El servidor apunta a otro dir | `systemctl --user cat dag_engine.service` -> ver `--dags-dir`. | Ajustar unit y `daemon-reload + restart`. |
| Cache UI antiguo | C++: pulsa `Refresh`. Web: `Ctrl+F5`. | — |
### 5.2. Validation: FAIL
`validate` muestra `parse error: ...` o `Validation: FAIL`. Causas tipicas:
| Mensaje | Causa | Fix |
|---|---|---|
| `yaml unmarshal: ...` | Sintaxis YAML rota (indentacion, tab vs espacios). | Usar 2 espacios consistentes. Validar online con `yamllint`. |
| `dag_parse: step[N]: name is required` | Step sin `name:`. | Anadir `name:`. |
| `dag_parse: step[N]: command or script required` | Step sin `command` ni `script`. | Anadir uno de los dos. |
| `cycle detected: A -> B -> A` | `depends` forma ciclo. | Romper la dependencia o convertir uno de los nodos en step distinto. |
| `unknown depends: <step>` | `depends:` referencia un step inexistente. | Comprobar nombres exactos (case-sensitive). |
| `invalid cron: <expr>` | Cron mal formado (4 o 6 campos en vez de 5). | Verificar `0 9 * * *` (5 campos). |
### 5.3. El DAG corre pero un step falla
**Sintoma:** `status: failed` en la UI.
1. Abre `DAG Detail` y haz doble-click en el run rojo -> `Run Detail`.
2. Expande el step que fallo (CollapsingHeader). Muestra `stdout` + `stderr`.
3. Errores tipicos:
| stderr | Causa | Fix |
|---|---|---|
| `command not found` | `command:` apunta a un binario fuera de `PATH`. | Path absoluto o setear `env: [PATH: ...]`. |
| `permission denied` | Script sin `chmod +x`. | `chmod +x <script>` (o usar `bash <script>`). |
| `no such file or directory` | `working_dir:` mal o ruta relativa rota. | Path absoluto en `working_dir:`. |
| Timeout | Step duro mas que `timeout_sec`. | Subir el limite o partir el step. |
| Exit 137 / OOM kill | Out-of-memory. | Reducir batch o anadir swap. |
### 5.4. El scheduler no dispara
**Sintoma:** Hay `schedule:` valido pero el DAG no corre solo.
1. Verifica que el server arranco con `--scheduler`:
```bash
systemctl --user cat dag_engine.service | grep scheduler
```
2. Logs:
```bash
journalctl --user-unit dag_engine.service -n 50 --no-pager | grep -E "scheduler|ticker"
```
Debes ver `[scheduler] ticker started for <name> (<cron>), next: <ISO8601>`.
3. Si `next:` es muy lejano (ej. en una semana) y necesitas probar -> dispara manual:
```bash
curl -X POST http://127.0.0.1:8090/api/dags/<name>/run
```
4. Hora del sistema descalibrada:
```bash
timedatectl status
```
Si difiere de la hora real, `sudo timedatectl set-ntp true`.
### 5.5. El frontend C++ no conecta WS
**Sintoma:** Panel `Live (WS)` muestra `disconnected`.
| Causa | Fix |
|---|---|
| Servidor caido | `systemctl --user status dag_engine.service`, `restart` si `inactive`. |
| Puerto cambiado | El cliente apunta a `127.0.0.1:8090` por codigo (constante `g_ws_port`). Reedificar si cambiaste el puerto del server. |
| Firewall Windows -> WSL | WSL2 expone `localhost`, normalmente OK. Si falla: `wsl --shutdown` y reabrir. |
### 5.6. Cleanup de runs viejos
`dag_runs` y `dag_step_results` crecen sin limite. Para limpiar:
```bash
sqlite3 apps/dag_engine/dag_engine.db <<'SQL'
DELETE FROM dag_step_results WHERE run_id IN (
SELECT id FROM dag_runs WHERE started_at < datetime('now', '-30 days')
);
DELETE FROM dag_runs WHERE started_at < datetime('now', '-30 days');
VACUUM;
SQL
```
### 5.7. Restaurar desde backup
Si rompes `dags_migrated/`, recupera desde el snapshot de `backup_all_bash_pipelines` (BACKUP_ROOT por defecto `~/backups/fn_registry`):
```bash
cp ~/backups/fn_registry/registry/daily.0/dags_migrated/*.yaml \
apps/dag_engine/dags_migrated/ 2>/dev/null || \
git checkout HEAD -- apps/dag_engine/dags_migrated/
systemctl --user restart dag_engine.service
```
---
## 6. Endpoints HTTP
| Metodo | Path | Que |
|---|---|---|
| GET | `/api/dags` | Lista DAGs + last_run + last_runs[5]. |
| GET | `/api/dags/{name}` | Detalle + validation. |
| POST | `/api/dags/{name}/run` | Dispara ejecucion (trigger=`api`). Devuelve `run_id`. |
| GET | `/api/runs` | Historial. Query: `dag`, `limit`, `offset`. |
| GET | `/api/runs/{id}` | Detalle de un run + sus step_results. |
| GET | `/api/ws/dagruns` | WebSocket. Snapshot + deltas en vivo (issue 0095). |
| GET | `/api/scheduler/status` | Tickers activos. |
| POST | `/api/scheduler/start` | Arranca scheduler (si no estaba). |
| POST | `/api/scheduler/stop` | Para scheduler. |
---
## 7. Referencias
- Schema parser: `functions/core/dag_parse.go` (frontmatter en `dag_parse_go_core`).
- Validator: `functions/core/dag_validate.go` (`dag_validate_go_core`).
- Topo sort: `functions/core/dag_topo_sort.go` (`dag_topo_sort_go_core`).
- Cron: `functions/core/parse_cron_expr.go` + `next_cron_time.go`.
- Frontend C++: `cpp/apps/dag_engine_ui/` (issue 0095).
- WS hub: `apps/dag_engine/events.go`.
- dag_engine es el scheduler oficial del ecosistema. Single-binary Go + SQLite, sin dependencias externas.
+9 -1
View File
@@ -6,7 +6,7 @@ import (
)
// RegisterAPI sets up all HTTP routes on the given mux.
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, frontendFS fs.FS) {
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, hub *DagRunHub, frontendFS fs.FS) {
// API routes.
mux.HandleFunc("GET /api/dags", handleListDags(executor))
mux.HandleFunc("GET /api/dags/{name}", handleGetDag(executor))
@@ -15,10 +15,18 @@ func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, f
mux.HandleFunc("GET /api/runs", handleListRuns(executor))
mux.HandleFunc("GET /api/runs/{id}", handleGetRun(executor))
// Function lookup proxy a registry.db (read-only).
mux.HandleFunc("GET /api/functions/{id}", handleGetFunction())
mux.HandleFunc("POST /api/scheduler/start", handleSchedulerStart(scheduler))
mux.HandleFunc("POST /api/scheduler/stop", handleSchedulerStop(scheduler))
mux.HandleFunc("GET /api/scheduler/status", handleSchedulerStatus(scheduler))
// Live updates (WS hub).
if hub != nil {
mux.HandleFunc("GET /api/ws/dagruns", handleDagRunsWS(hub))
}
// Frontend SPA fallback.
if frontendFS != nil {
mux.Handle("/", spaHandler(frontendFS))
+65 -7
View File
@@ -2,7 +2,8 @@
name: dag_engine
lang: go
domain: infra
description: "Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite."
version: 0.1.0
description: "Motor de ejecucion de DAGs del fn_registry: CLI + servidor HTTP + scheduler cron. Schema YAML propio con `function:` para invocar funciones del registry (`fn run <id>`) y `command:` para shell. Historial en SQLite. Scheduler oficial del ecosistema."
tags: [service, dag, workflow, scheduler, web, cron]
uses_functions:
- dag_parse_go_core
@@ -27,6 +28,18 @@ uses_types:
framework: "net/http + vite + react"
entry_point: "main.go"
dir_path: "apps/dag_engine"
service:
port: 8090
health_endpoint: /api/dags
health_timeout_s: 3
systemd_unit: dag_engine.service
systemd_scope: user
restart_policy: always
runtime: systemd-user
pc_targets:
- aurgi-pc
- home-wsl
is_local_only: false
---
## Arquitectura
@@ -71,15 +84,60 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
```bash
# CLI
./dag-engine run ~/dagu/dags/example.yaml
./dag-engine list ~/dagu/dags/
./dag-engine run apps/dag_engine/dags_migrated/fn_backup.yaml
./dag-engine list apps/dag_engine/dags_migrated/
# Servidor web
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
# Servidor web (production: gestionado por dag_engine.service systemd user unit)
./dag-engine server --port 8090 --dags-dir apps/dag_engine/dags_migrated/ --scheduler
# Browser: http://localhost:8090
```
## Notas
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
Puerto por defecto 8090 (mismo que Dagu).
Schema YAML propio (ver `README.md` seccion 3 + ejemplos en `dags_migrated/`). Steps tipo `function:` invocan `fn run <id>` y propagan `function_id` a `dag_step_results` para el bucle reactivo. Puerto default 8090.
### 2026-05-16 — Fix function-not-found en steps `function:` + panel Logs en RunDetail `[done]`
Sintoma: `fn_backup` y `daily-registry-audit` fallaron 3 noches seguidas con `error: function "<id>" not found (tried as ID and name)` aunque las funciones existen en `registry.db` raiz.
Raiz: servicio systemd `dag_engine.service` tiene `WorkingDirectory=/home/lucas/fn_registry/apps/dag_engine`. Binario `fn` resuelve `registry.db` por (1) `FN_REGISTRY_ROOT`, (2) `root()` walk-up buscando `go.mod`, (3) exe dir (`cmd/fn/ops.go:1597-1628`). Sin `FN_REGISTRY_ROOT` seteado, (2) encuentra el `go.mod` de `apps/dag_engine/` y devuelve ese dir — donde habia una copia stale `apps/dag_engine/registry.db` (262 KB, May 15) sin las funciones recien creadas. Viola regla `.claude/rules/db_locations.md` (registry.db SOLO en raiz).
Fix:
- Borrado `apps/dag_engine/registry.db` stale.
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT=/home/lucas/fn_registry`, `FN_BIN=/home/lucas/fn_registry/fn`, `PATH=/usr/local/go/bin:/home/lucas/go/bin:...`, `HOME=/home/lucas`. Sin PATH el step `go vet` fallaba con `exec: "go": executable file not found in $PATH`.
- `apps/dag_engine/executor.go`: para steps `function:` el spawn exporta `FN_REGISTRY_ROOT=<root>` en env y, si `step.dir`/`working_dir` vacios, fija `dir = fnRegistryRoot`. Belt-and-suspenders: aunque alguien lance el binario sin systemd, los `function:` steps usan el root canonico.
Verificacion: `POST /api/dags/daily-registry-audit/run` -> step `audit_capabilities` pasa (387 ms) en vez de fallar con not-found. Restantes failures (`audit_artefacts` exit 1, `fn_backup` exit 4 sin respetar `continue_on.exit_code`) son bugs reales independientes — fuera de scope.
### 2026-05-16 — Panel Logs en RunDetail (frontend) `[done]`
- `apps/dag_engine/frontend/src/pages/RunDetail.tsx`: nuevo `<Paper>` "Logs" al final con `<Code block>` scrollable (max-h 480) + `CopyButton` de Mantine (icono toggle copy/check teal).
- Helper `buildLogText(run, steps)` compone texto plano: metadata del run (dag, path, status, trigger, started/finished ISO, duration ms, error) + por step (`[status] name exit=N Nms`, started, finished, error, stdout, stderr indentado 4 espacios).
- Permite pegar log entero al LLM para debugging sin abrir N collapses del `StepTimeline`.
- Build frontend pendiente: `pnpm build` rompe por errores preexistentes (`StepTimeline.tsx:49` usa API legacy `<Collapse in={opened}>`; `main.tsx:1` importa `@mantine/core/styles.css` sin tipos). Edit de RunDetail type-checkea limpio.
### 2026-05-16 — BBDDs canonicas (referencia rapida)
- `dag_engine.db`: `apps/dag_engine/dag_engine.db` (+ WAL sidecars). Migrations en `apps/dag_engine/store/migrations/` (`001_init.sql`, `002_step_function_id.sql`). Tablas `dag_runs`, `dag_step_results`.
- NO debe coexistir copia de `registry.db` en este dir (viola `db_locations.md`). Si reaparece: borrarla.
## Lo siguiente que pega
- `audit_artefacts` falla con exit 1 en `daily-registry-audit` — investigar stderr real (probablemente artefacto huerfano o git drift). Step independiente, no bloquea el resto del DAG.
- `fn_backup` step `run_backup_all` sale con exit 4 y el DAG no respeta `continue_on.exit_code: [4]`. Bug en executor: parsear `step.ContinueOn.ExitCode []int` y comparar con `result.ExitCode`. Hoy solo se mira `step.ContinueOn.Failure` (bool).
- Frontend `pnpm build` roto por API drift de Mantine en `StepTimeline.tsx` (`<Collapse in={opened}>`) y CSS type import en `main.tsx`. Fix junto con un refresh general de tipos.
## Documentacion de usuario
Guia completa (formato YAML, anadir DAGs, troubleshooting, endpoints HTTP):
**[apps/dag_engine/README.md](README.md)**.
## Capability growth log
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
- `patch`: bugfix sin cambio observable.
- v0.1.0 (2026-05-18) — baseline.
@@ -0,0 +1,26 @@
name: fn_backup
description: Backup diario de fn_registry (registry.db + operations.db + vaults) via funcion del registry
schedule:
- "0 3 * * *"
tags: [backup, registry, daily]
env:
- BACKUP_ROOT: /home/lucas/backups/fn_registry
steps:
- name: ensure_dirs
command: mkdir -p ${BACKUP_ROOT}
- name: run_backup_all
description: "Snapshot atomico de registry.db + operations.db + vaults con retention 7/4/12"
function: backup_all_bash_pipelines
args: ["${BACKUP_ROOT}"]
continue_on:
exit_code: [4]
depends: [ensure_dirs]
- name: report_status
command: bash -c 'ls -lh ${BACKUP_ROOT}/registry/daily.0 ${BACKUP_ROOT}/operations/*/daily.0 2>/dev/null | tail -20'
depends: [run_backup_all]
@@ -0,0 +1,51 @@
name: revision-viernes-finanzas
description: Revisión semanal de finanzas personales - ingesta, informe y push a Gitea
tags: [finanzas, semanal]
type: graph
schedule: "0 9 * * 5"
env:
- PROJECT_DIR: /home/lucas/analysis/finanzas_personales
- PYTHON: /home/lucas/analysis/finanzas_personales/.venv/bin/python
handler_on:
failure:
command: echo "[$(date)] FALLÓ revision-viernes-finanzas" >> /home/lucas/dagu/logs/failures.log
steps:
- id: ingest
description: Procesar archivos nuevos del inbox (BBVA xlsx + Revolut csv)
working_dir: ${PROJECT_DIR}
command: ./bin/ingest -skip-notebooks
continue_on:
failure: true
- id: informe
description: Generar informe semanal de cumplimiento del presupuesto
command: ${PYTHON} /home/lucas/dagu/scripts/informe_finanzas.py
depends: [ingest]
- id: git_push
description: Commit y push del informe a Gitea
working_dir: ${PROJECT_DIR}
script: |
#!/bin/bash
set -euo pipefail
if git diff --quiet data/04_output/informe_semanal.md 2>/dev/null && \
! git ls-files --others --exclude-standard | grep -q informe_semanal.md; then
echo "Sin cambios en el informe, skip push"
exit 0
fi
git add data/04_output/informe_semanal.md
git add data/03_processed/ 2>/dev/null || true
git commit -m "Informe semanal $(date +%Y-%m-%d)
Co-Authored-By: Dagu Automation <noreply@dagu.dev>"
git push origin master:main
echo "Push completado"
depends: [informe]
+438
View File
@@ -0,0 +1,438 @@
package main
// WebSocket hub para live updates de dag_runs + dag_step_results.
// Patron: sqlite_api/events.go (CallMonitorHub) — issue 0095.
//
// Diseño:
// - Hub global con N subscribers WS.
// - Ticker arranca solo con >=1 subscriber. Cero overhead si nadie mira.
// - Cada tick (500ms): query rowid>watermark + activos (status running/pending)
// + recientes finished (ultimos 5s) -> broadcast upsert.
// - Snapshot inicial: lista de DAGs + ultimos 50 runs + step_results.
// - El cliente trata `runs` y `steps` como upserts por id.
import (
"context"
"database/sql"
"log"
"net/http"
"sync"
"time"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"dag-engine/store"
)
const (
dagWSTickInterval = 500 * time.Millisecond
dagWSTickIntervalIdle = 2 * time.Second
dagWSIdleThreshold = 30 * time.Second
dagWSSnapshotRuns = 50
dagWSBroadcastTimeout = 2 * time.Second
dagWSRecentFinishedS = 5
)
type wsRun struct {
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
}
type wsStep struct {
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt string `json:"started_at,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
type wsWatermark struct {
Runs int64 `json:"runs"`
Steps int64 `json:"steps"`
}
type wsDagMessage struct {
Type string `json:"type"` // snapshot|delta|ping
Watermark wsWatermark `json:"watermark"`
Dags []DagInfo `json:"dags,omitempty"`
Runs []wsRun `json:"runs,omitempty"`
Steps []wsStep `json:"steps,omitempty"`
ServerTime int64 `json:"server_time"`
}
type wsDagClientCmd struct {
Watermark wsWatermark `json:"watermark,omitempty"`
}
type dagSubscriber struct {
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
out chan wsDagMessage
watermark wsWatermark
}
// DagRunHub broadcastea cambios de dag_runs + dag_step_results a clientes WS.
type DagRunHub struct {
db *store.DB
executor *Executor
mu sync.Mutex
subscribers map[*dagSubscriber]struct{}
tickerStop chan struct{}
tickerOn bool
watermark wsWatermark
lastEventAt time.Time
}
func NewDagRunHub(db *store.DB, executor *Executor) *DagRunHub {
return &DagRunHub{
db: db,
executor: executor,
subscribers: make(map[*dagSubscriber]struct{}),
}
}
func (h *DagRunHub) register(s *dagSubscriber) {
h.mu.Lock()
h.subscribers[s] = struct{}{}
shouldStart := !h.tickerOn
if shouldStart {
h.tickerStop = make(chan struct{})
h.tickerOn = true
h.lastEventAt = time.Now()
}
h.mu.Unlock()
if shouldStart {
go h.tickerLoop()
}
}
func (h *DagRunHub) unregister(s *dagSubscriber) {
h.mu.Lock()
if _, ok := h.subscribers[s]; !ok {
h.mu.Unlock()
return
}
delete(h.subscribers, s)
close(s.out)
shouldStop := h.tickerOn && len(h.subscribers) == 0
if shouldStop {
close(h.tickerStop)
h.tickerOn = false
}
h.mu.Unlock()
}
func (h *DagRunHub) tickerLoop() {
interval := dagWSTickInterval
t := time.NewTimer(interval)
defer t.Stop()
for {
select {
case <-h.tickerStop:
return
case <-t.C:
runs, steps, wm, err := h.fetchDelta(h.getWatermark())
if err != nil {
log.Printf("[dagws] fetchDelta: %v", err)
} else if len(runs) > 0 || len(steps) > 0 {
h.setWatermark(wm)
h.recordActivity()
h.broadcast(wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
})
}
if time.Since(h.lastActivityAt()) > dagWSIdleThreshold {
interval = dagWSTickIntervalIdle
} else {
interval = dagWSTickInterval
}
t.Reset(interval)
}
}
}
func (h *DagRunHub) getWatermark() wsWatermark {
h.mu.Lock()
defer h.mu.Unlock()
return h.watermark
}
func (h *DagRunHub) setWatermark(v wsWatermark) {
h.mu.Lock()
if v.Runs > h.watermark.Runs {
h.watermark.Runs = v.Runs
}
if v.Steps > h.watermark.Steps {
h.watermark.Steps = v.Steps
}
h.mu.Unlock()
}
func (h *DagRunHub) recordActivity() {
h.mu.Lock()
h.lastEventAt = time.Now()
h.mu.Unlock()
}
func (h *DagRunHub) lastActivityAt() time.Time {
h.mu.Lock()
defer h.mu.Unlock()
return h.lastEventAt
}
// fetchDelta devuelve runs/steps con (rowid > watermark) OR (status in-flight)
// OR (recently finished). Watermark devuelto = max rowid visto.
func (h *DagRunHub) fetchDelta(since wsWatermark) ([]wsRun, []wsStep, wsWatermark, error) {
conn := h.db.Conn()
if conn == nil {
return nil, nil, since, nil
}
cutoff := time.Now().Add(-time.Duration(dagWSRecentFinishedS) * time.Second).Format(time.RFC3339)
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Runs, cutoff)
if err != nil {
return nil, nil, since, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Steps, cutoff)
if err != nil {
return runs, nil, since, err
}
out := wsWatermark{Runs: maxRuns, Steps: maxSteps}
if out.Runs < since.Runs {
out.Runs = since.Runs
}
if out.Steps < since.Steps {
out.Steps = since.Steps
}
return runs, steps, out, nil
}
// fetchSnapshot devuelve DAGs + ultimos N runs + sus step_results + watermark.
func (h *DagRunHub) fetchSnapshot() ([]DagInfo, []wsRun, []wsStep, wsWatermark, error) {
dags, err := h.executor.ListDAGs()
if err != nil {
log.Printf("[dagws] list dags: %v", err)
dags = nil
}
conn := h.db.Conn()
if conn == nil {
return dags, nil, nil, wsWatermark{}, nil
}
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
ORDER BY started_at DESC
LIMIT ?`, dagWSSnapshotRuns)
if err != nil {
return dags, nil, nil, wsWatermark{}, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE run_id IN (SELECT id FROM dag_runs ORDER BY started_at DESC LIMIT ?)
ORDER BY rowid ASC`, dagWSSnapshotRuns)
if err != nil {
return dags, runs, nil, wsWatermark{Runs: maxRuns}, err
}
return dags, runs, steps, wsWatermark{Runs: maxRuns, Steps: maxSteps}, nil
}
func scanRuns(conn *sql.DB, q string, args ...any) ([]wsRun, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsRun
var max int64
for rows.Next() {
var r wsRun
var rowid int64
if err := rows.Scan(&rowid, &r.ID, &r.DagName, &r.DagPath, &r.Status,
&r.Trigger, &r.StartedAt, &r.FinishedAt, &r.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, r)
}
return out, max, rows.Err()
}
func scanSteps(conn *sql.DB, q string, args ...any) ([]wsStep, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsStep
var max int64
for rows.Next() {
var s wsStep
var rowid int64
if err := rows.Scan(&rowid, &s.ID, &s.RunID, &s.StepName, &s.Status,
&s.ExitCode, &s.Stdout, &s.Stderr, &s.StartedAt, &s.FinishedAt,
&s.DurationMs, &s.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, s)
}
return out, max, rows.Err()
}
func (h *DagRunHub) broadcast(msg wsDagMessage) {
h.mu.Lock()
subs := make([]*dagSubscriber, 0, len(h.subscribers))
for s := range h.subscribers {
subs = append(subs, s)
}
h.mu.Unlock()
for _, s := range subs {
select {
case s.out <- msg:
default:
log.Printf("[dagws] dropping frame for slow subscriber")
}
}
}
// handleDagRunsWS upgrade WS y gestiona lifecycle.
// Endpoint: GET /api/ws/dagruns
func handleDagRunsWS(hub *DagRunHub) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
if err != nil {
log.Printf("[dagws] accept: %v", err)
return
}
defer conn.Close(websocket.StatusInternalError, "closing")
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
sub := &dagSubscriber{
conn: conn,
ctx: ctx,
cancel: cancel,
out: make(chan wsDagMessage, 64),
}
hub.register(sub)
defer hub.unregister(sub)
dags, runs, steps, wm, err := hub.fetchSnapshot()
if err != nil {
log.Printf("[dagws] snapshot: %v", err)
conn.Close(websocket.StatusInternalError, "snapshot failed")
return
}
hub.setWatermark(wm)
initial := wsDagMessage{
Type: "snapshot",
Watermark: wm,
Dags: dags,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}
if err := wsjson.Write(ctx, conn, initial); err != nil {
return
}
readErr := make(chan error, 1)
go func() {
for {
var cmd wsDagClientCmd
if err := wsjson.Read(ctx, conn, &cmd); err != nil {
readErr <- err
return
}
if cmd.Watermark.Runs > 0 || cmd.Watermark.Steps > 0 {
runs, steps, wm, err := hub.fetchDelta(cmd.Watermark)
if err == nil && (len(runs) > 0 || len(steps) > 0) {
hub.setWatermark(wm)
select {
case sub.out <- wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}:
default:
}
}
}
}
}()
for {
select {
case <-ctx.Done():
return
case err := <-readErr:
if err != nil {
return
}
case msg, ok := <-sub.out:
if !ok {
return
}
wctx, wcancel := context.WithTimeout(ctx, dagWSBroadcastTimeout)
err := wsjson.Write(wctx, conn, msg)
wcancel()
if err != nil {
return
}
}
}
}
}
+53 -20
View File
@@ -156,22 +156,49 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
stepID := generateID()
now := time.Now()
// Resolve command source: function (registry) takes precedence over command/script.
var command string
var stepFunctionID string
var fnRegistryRoot string
if step.Function != "" {
stepFunctionID = step.Function
fnRegistryRoot = os.Getenv("FN_REGISTRY_ROOT")
if fnRegistryRoot == "" {
fnRegistryRoot = "/home/lucas/fn_registry"
}
fnBin := os.Getenv("FN_BIN")
if fnBin == "" {
fnBin = fnRegistryRoot + "/fn"
}
parts := []string{fnBin, "run", step.Function}
parts = append(parts, step.Args...)
command = strings.Join(parts, " ")
} else if step.Command != "" {
command = step.Command
} else if step.Script != "" {
command = step.Script
}
e.store.InsertStepResult(&store.DagStepResult{
ID: stepID,
RunID: runID,
StepName: stepName(step),
Status: "running",
StartedAt: &now,
ID: stepID,
RunID: runID,
StepName: stepName(step),
FunctionID: stepFunctionID,
Status: "running",
StartedAt: &now,
})
// Build environment.
env := buildStepEnv(dag, step, daguEnvPath, outputs)
// Determine command.
command := step.Command
if command == "" && step.Script != "" {
command = step.Script
// For function: steps, force FN_REGISTRY_ROOT into env so `fn run`
// resolves the canonical registry.db (not whatever lives at the spawn cwd).
// Prevents the apps/dag_engine/registry.db stale-shadow bug (2026-05-16).
if stepFunctionID != "" {
env = append(env, "FN_REGISTRY_ROOT="+fnRegistryRoot)
}
if command == "" {
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
return nil
@@ -182,11 +209,15 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
command = resolveStepRefs(command, outputs)
mu.Unlock()
// Determine working directory.
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
// so `fn` resolves registry.db correctly via go.mod walk-up.
dir := step.Dir
if dir == "" {
dir = dag.WorkingDir
}
if dir == "" && stepFunctionID != "" {
dir = fnRegistryRoot
}
shell := step.Shell
if shell == "" {
@@ -326,14 +357,15 @@ func generateID() string {
// DagInfo summarizes a DAG file for listing.
type DagInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
LastRuns []store.DagRun `json:"last_runs,omitempty"` // 5 mas recientes (mas reciente primero)
}
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
@@ -379,10 +411,11 @@ func (e *Executor) ListDAGs() ([]DagInfo, error) {
Valid: true,
}
// Attach last run info.
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
// Attach last 5 runs (most recent first).
runs, _, _ := e.store.ListRuns(dag.Name, 5, 0)
if len(runs) > 0 {
info.LastRun = &runs[0]
info.LastRuns = runs
}
dags = append(dags, info)
@@ -9,12 +9,63 @@ import {
Paper,
Alert,
Loader,
CopyButton,
Tooltip,
ActionIcon,
Code,
} from "@mantine/core";
import { IconArrowLeft } from "@tabler/icons-react";
import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react";
import { getRun } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import { StepTimeline } from "../components/StepTimeline";
import type { RunDetail as RunDetailType } from "../types";
import type { RunDetail as RunDetailType, DagStepResult, DagRun } from "../types";
function buildLogText(run: DagRun, steps: DagStepResult[]): string {
const lines: string[] = [];
const started = run.StartedAt ? new Date(run.StartedAt) : null;
const finished = run.FinishedAt ? new Date(run.FinishedAt) : null;
const durationMs =
started && finished ? finished.getTime() - started.getTime() : null;
lines.push(`=== DAG run ${run.ID} ===`);
lines.push(`dag: ${run.DagName}`);
lines.push(`path: ${run.DagPath}`);
lines.push(`status: ${run.Status}`);
lines.push(`trigger: ${run.Trigger}`);
lines.push(`started: ${started ? started.toISOString() : "-"}`);
lines.push(`finished: ${finished ? finished.toISOString() : "-"}`);
lines.push(
`duration: ${durationMs !== null ? `${durationMs} ms` : "running..."}`
);
if (run.Error) {
lines.push("");
lines.push("run error:");
lines.push(run.Error);
}
lines.push("");
lines.push(`--- steps (${steps.length}) ---`);
for (const s of steps) {
lines.push("");
lines.push(
`[${s.Status}] ${s.StepName} exit=${s.ExitCode} ${s.DurationMs}ms`
);
if (s.StartedAt) lines.push(` started: ${s.StartedAt}`);
if (s.FinishedAt) lines.push(` finished: ${s.FinishedAt}`);
if (s.Error) {
lines.push(" error:");
lines.push(s.Error.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stdout) {
lines.push(" stdout:");
lines.push(s.Stdout.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stderr) {
lines.push(" stderr:");
lines.push(s.Stderr.split("\n").map((l) => " " + l).join("\n"));
}
}
return lines.join("\n");
}
export function RunDetail() {
const { id } = useParams<{ id: string }>();
@@ -100,6 +151,41 @@ export function RunDetail() {
</Text>
)}
</Paper>
<Paper p="md" withBorder>
<Group justify="space-between" mb="sm">
<Title order={4}>Logs</Title>
<CopyButton value={buildLogText(run, steps || [])} timeout={1500}>
{({ copied, copy }) => (
<Tooltip
label={copied ? "Copiado" : "Copiar log completo"}
withArrow
position="left"
>
<ActionIcon
variant={copied ? "filled" : "light"}
color={copied ? "teal" : "blue"}
onClick={copy}
aria-label="Copiar logs"
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Code
block
style={{
maxHeight: 480,
overflow: "auto",
whiteSpace: "pre",
fontSize: 12,
}}
>
{buildLogText(run, steps || [])}
</Code>
</Paper>
</Stack>
);
}
+9 -6
View File
@@ -5,6 +5,7 @@ go 1.25.0
require (
fn-registry v0.0.0-00010101000000-000000000000
github.com/mattn/go-sqlite3 v1.14.37
nhooyr.io/websocket v1.8.17
)
require (
@@ -28,19 +29,21 @@ require (
github.com/marcboeker/go-duckdb v1.8.5 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+18 -10
View File
@@ -111,44 +111,50 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -166,3 +172,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+4 -4
View File
@@ -30,10 +30,10 @@ func handleGetDag(executor *Executor) http.HandlerFunc {
runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0)
resp := map[string]interface{}{
"info": info,
"dag": dag,
"validation": validation,
"runs": runs,
"info": info,
"dag": dag,
"validation": validation,
"recent_runs": runs,
}
writeJSON(w, http.StatusOK, resp)
}
+2 -1
View File
@@ -286,6 +286,7 @@ func cmdServer(args []string) {
executor := NewExecutor(db, cfg.DagsDir)
scheduler := NewScheduler(executor, cfg.DagsDir)
dagRunHub := NewDagRunHub(db, executor)
// Prepare frontend FS.
var feFS iofs.FS
@@ -303,7 +304,7 @@ func cmdServer(args []string) {
}
mux := http.NewServeMux()
RegisterAPI(mux, executor, scheduler, feFS)
RegisterAPI(mux, executor, scheduler, dagRunHub, feFS)
handler := corsMiddleware(loggingMiddleware(mux))
+63 -28
View File
@@ -2,15 +2,43 @@ package store
import (
"database/sql"
_ "embed"
"embed"
"fmt"
"io/fs"
"sort"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migrations/001_init.sql
var migrationSQL string
//go:embed migrations/*.sql
var migrationsFS embed.FS
// applyMigrations executes every embedded migrations/*.sql in order.
// Each statement is idempotent (IF NOT EXISTS / ADD COLUMN). Duplicate-column
// errors from re-running ALTER TABLE ADD COLUMN are tolerated.
func applyMigrations(conn *sql.DB) error {
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
if err != nil {
return err
}
sort.Strings(files)
for _, f := range files {
b, err := migrationsFS.ReadFile(f)
if err != nil {
return fmt.Errorf("%s: read: %w", f, err)
}
if _, err := conn.Exec(string(b)); err != nil {
if strings.Contains(err.Error(), "duplicate column") ||
strings.Contains(err.Error(), "already exists") {
continue
}
return fmt.Errorf("%s: %w", f, err)
}
}
return nil
}
// DB wraps a SQLite connection for DAG run persistence.
type DB struct {
@@ -24,7 +52,7 @@ func Open(path string) (*DB, error) {
if err != nil {
return nil, fmt.Errorf("store: open %s: %w", path, err)
}
if _, err := conn.Exec(migrationSQL); err != nil {
if err := applyMigrations(conn); err != nil {
conn.Close()
return nil, fmt.Errorf("store: migrate: %w", err)
}
@@ -36,18 +64,24 @@ func (db *DB) Close() error {
return db.conn.Close()
}
// Conn exposes the underlying *sql.DB for read-only queries from other
// packages (e.g. WS hub in events.go). Do not Close() the returned conn.
func (db *DB) Conn() *sql.DB {
return db.conn
}
// --- DagRun CRUD ---
// DagRun mirrors infra.DagRun for the store layer.
type DagRun struct {
ID string
DagName string
DagPath string
Status string
Trigger string
StartedAt time.Time
FinishedAt *time.Time
Error string
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
}
// CreateRun inserts a new run record.
@@ -123,17 +157,18 @@ func (db *DB) ListRuns(dagName string, limit, offset int) ([]DagRun, int, error)
// DagStepResult mirrors infra.DagStepResult for the store layer.
type DagStepResult struct {
ID string
RunID string
StepName string
Status string
ExitCode int
Stdout string
Stderr string
StartedAt *time.Time
FinishedAt *time.Time
DurationMs int64
Error string
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
FunctionID string `json:"function_id,omitempty"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
// InsertStepResult inserts a new step result.
@@ -148,9 +183,9 @@ func (db *DB) InsertStepResult(r *DagStepResult) error {
finishedAt = &s
}
_, err := db.conn.Exec(
`INSERT INTO dag_step_results (id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.Status, r.ExitCode, r.Stdout, r.Stderr,
`INSERT INTO dag_step_results (id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.FunctionID, r.Status, r.ExitCode, r.Stdout, r.Stderr,
startedAt, finishedAt, r.DurationMs, r.Error,
)
return err
@@ -173,7 +208,7 @@ func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr s
// ListStepResults returns all step results for a given run.
func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
rows, err := db.conn.Query(
`SELECT id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
`SELECT id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID,
)
if err != nil {
@@ -185,7 +220,7 @@ func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
for rows.Next() {
var r DagStepResult
var startedAt, finishedAt sql.NullString
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode,
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.FunctionID, &r.Status, &r.ExitCode,
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
return nil, err
}
+15 -1
View File
@@ -2,6 +2,7 @@
name: shaders_lab
lang: cpp
domain: gfx
version: 0.1.0
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
uses_functions:
@@ -30,8 +31,11 @@ uses_types:
- dag_types_cpp_gfx
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/shaders_lab"
dir_path: "apps/shaders_lab"
repo_url: ""
icon:
phosphor: "palette"
accent: "#ea580c"
---
## Arquitectura
@@ -102,3 +106,13 @@ cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w6
- El boton "Save as generator" valida snake_case, evita colisionar con
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
## Capability growth log
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
- `patch`: bugfix sin cambio observable.
- v0.1.0 (2026-05-18) — baseline.
@@ -6,16 +6,21 @@
scan_secrets_in_dirty() {
local repo_dir="${1:-.}"
if [[ ! -d "$repo_dir/.git" ]]; then
# Accept both regular repos (.git is a directory) and worktrees (.git is a
# file containing "gitdir: ..." pointer).
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
return 1
fi
# Listar archivos modificados o nuevos (excluyendo borrados)
# y filtrar por patron de secret en el nombre del archivo
# y filtrar por patron de secret en el nombre del archivo.
# Excluye extensiones de codigo (sh/go/py/ts/md/etc) para no marcar el
# propio scanner ni docs que hablen de "secret"/"token".
git -C "$repo_dir" status --porcelain \
| awk '{print $NF}' \
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
| grep -Ev '\.(sh|go|py|ts|tsx|js|jsx|md|rs|cpp|h|hpp|c|java|rb|html|css)$' \
|| true
}
@@ -3,7 +3,7 @@ name: deploy_cpp_exe_to_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "deploy_cpp_exe_to_windows(app_name: string, app_dir: string) -> void"
description: "Copia el .exe de Windows (compilado por build_cpp_windows) y sus assets al escritorio de Windows /mnt/c/Users/lucas/Desktop/apps/<APP>/. Mata el proceso si esta corriendo (taskkill.exe pre-autorizado), copia DLLs, sincroniza assets/ y enrichers/ con rsync, maneja runtime Python embebido si python_runtime: true en app.md, y copia extras gx-cli. Preserva siempre local_files/ (estado del usuario)."
@@ -63,3 +63,8 @@ Desktop/apps/<APP>/
- `rsync --delete` en assets/ y enrichers/ para mantener destino limpio.
- Si `python_runtime: true` en `app.md` y `runtime/.lock` es mas antiguo que `app.md`, invoca `tools/freeze_python_runtime.sh` automaticamente.
- `local_files/` jamas se toca: contiene estado per-PC del usuario (DBs SQLite, ImGui layouts, settings).
## Capability growth log
v1.1.0 (2026-05-17) — Bugfix: el `cp` del .exe no chequeaba exit status y la funcion reportaba OK aunque fallase por "Permission denied" (proceso aun vivo). Ahora: (1) tras `taskkill.exe`, poll de hasta 3s sobre `tasklist.exe` esperando muerte real del proceso; (2) `cp` envuelto en retry 5 veces con backoff 0.5s y re-taskkill entre intentos; (3) si los 5 intentos fallan, `return 1` (antes: silently continued).
v1.0.0 — Initial.
@@ -30,12 +30,38 @@ deploy_cpp_exe_to_windows() {
mkdir -p "$dest" "$assets"
# --- 3. Pre-deploy: matar proceso si esta corriendo en Windows ---
# Windows libera el file handle async tras taskkill. Hacemos poll hasta que
# el proceso desaparezca de tasklist o se agote el timeout.
if command -v taskkill.exe >/dev/null 2>&1; then
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
if ! tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null \
| grep -qi "^${app}.exe"; then
break
fi
sleep 0.3
done
fi
# --- 4. Copiar .exe al top level ---
cp -v "$exe_src" "$dest/"
# --- 4. Copiar .exe al top level con retry ---
# Windows puede tener el archivo aun bloqueado momentaneamente; reintentar.
local cp_ok=0
local attempt
for attempt in 1 2 3 4 5; do
if cp -v "$exe_src" "$dest/"; then
cp_ok=1
break
fi
echo "deploy_cpp_exe_to_windows: cp intento $attempt fallo, reintentando..." >&2
# Reintentar taskkill por si el proceso resucito o quedo zombie.
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
sleep 0.5
done
if [ "$cp_ok" -ne 1 ]; then
echo "ERROR: cp del .exe fallo tras 5 intentos. $exe_src -> $dest/" >&2
return 1
fi
# --- 5. DLLs al top level (Windows DLL search convention) ---
find "$build_win/apps/$app" -maxdepth 1 -type f -name '*.dll' \
@@ -17,7 +17,9 @@ git_hook_audit_app_drift() {
echo "ERROR: repo_dir required" >&2
return 2
fi
if [[ ! -d "$repo_dir/.git" ]]; then
# Accept both regular repos (.git is a directory) and worktrees (.git is a
# file containing "gitdir: ..." pointer).
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
echo "ERROR: $repo_dir is not a git repo" >&2
return 2
fi
+11 -1
View File
@@ -3,7 +3,7 @@ name: launch_cpp_app_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "launch_cpp_app_windows(app_name: string, [desktop_dir: string]) -> void"
description: "Lanza un binario .exe en Windows desde WSL2. Asume que deploy_cpp_exe_to_windows ya copió el exe a Desktop/apps/<app_name>/. Usa cmd.exe /c start para desacoplar el proceso y retornar inmediatamente."
@@ -68,3 +68,13 @@ launch_cpp_app_windows "registry_dashboard"
```
No se incluyen tests automatizados porque requieren entorno WSL2 con Windows activo y no son automatizables en CI.
## Gotchas
- Si `FN_REGISTRY_ROOT_WSL` no es tu ruta default de fn_registry (`/home/<user>/fn_registry`), setea la variable antes de invocar esta función: `FN_REGISTRY_ROOT_WSL=/ruta/custom launch_cpp_app_windows <app>`.
- El proceso hijo hereda `FN_REGISTRY_ROOT` como path Windows (backslashes) y `FN_REGISTRY_ROOT_WSL` como path Linux. En el exe C++, `py_resolve_interpreter()` usa `FN_REGISTRY_ROOT_WSL` para construir el invocation `wsl.exe -- /path/python3`.
- PowerShell escapa `$` con `\$` para evitar expansión de variables en el string del comando.
## Capability growth log
- v1.1.0 (2026-05-16) — auto-propaga `FN_REGISTRY_ROOT` (Windows path) + `FN_REGISTRY_ROOT_WSL` (Linux path) al proceso hijo para que pueda invocar WSL python via `wsl.exe`.
+12 -2
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
# launch_cpp_app_windows — Lanza un .exe en Windows desde WSL2 via cmd.exe /c start.
# launch_cpp_app_windows v1.1.0 — Lanza un .exe en Windows desde WSL2 via PowerShell.
# Asume que el exe ya fue copiado por deploy_cpp_exe_to_windows al escritorio.
# v1.1.0: propaga FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
# al proceso hijo para que pueda invocar WSL python via wsl.exe.
launch_cpp_app_windows() {
local app="${1:-}"
@@ -26,10 +28,18 @@ launch_cpp_app_windows() {
win_app_dir=$(wslpath -w "$desktop_dir/apps/$app")
win_exe="$win_app_dir\\$app.exe"
# Deducir raiz del registry en Linux (WSL) y traducir a Windows path.
# FN_REGISTRY_ROOT_WSL puede sobreescribirse en el entorno del llamante.
local linux_root win_root
linux_root="${FN_REGISTRY_ROOT_WSL:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
win_root=$(wslpath -w "$linux_root")
# Start-Process detacha (equivale a `start` de cmd) y respeta -WorkingDirectory.
# Las comillas simples en PowerShell son literales — no procesa \ ni $.
# Se inyectan FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
# para que el exe pueda localizar el venv WSL y hacer: wsl.exe -- python3 ...
powershell.exe -NoProfile -Command \
"Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
"\$env:FN_REGISTRY_ROOT='$win_root'; \$env:FN_REGISTRY_ROOT_WSL='$linux_root'; Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
>/dev/null 2>&1
local ts
@@ -0,0 +1,58 @@
---
name: refresh_windows_icon_cache
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "refresh_windows_icon_cache() -> void"
description: "Fuerza a Windows Explorer a recargar la cache de iconos desde WSL2 via ie4uinit.exe. Best-effort: nunca aborta, retorna 0 si alguna estrategia tuvo exito."
tags: [windows, wsl, deploy, shell, icons, cpp-windows]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/refresh_windows_icon_cache.sh"
params: []
output: "0 si al menos una estrategia tuvo exito, non-zero si todas fallaron. Imprime una linea de estado en stdout."
---
## Ejemplo
```bash
source bash/functions/infra/refresh_windows_icon_cache.sh
refresh_windows_icon_cache
# icon cache refresh: ok via ie4uinit -show
```
O directamente via `fn run`:
```bash
./fn run refresh_windows_icon_cache_bash_infra
```
Uso tipico en un pipeline de redeploy tras reconstruir el `.exe`:
```bash
source bash/functions/infra/deploy_cpp_exe_to_windows.sh
source bash/functions/infra/refresh_windows_icon_cache.sh
deploy_cpp_exe_to_windows "registry_dashboard" "apps/registry_dashboard"
refresh_windows_icon_cache
```
## Cuando usarla
Despues de redeployar un `.exe` Windows cuyo `appicon.ico` cambio (via windres embebido en el build), antes de que Windows muestre el icono nuevo en taskbar, Alt+Tab y File Explorer. Sin esta llamada Windows puede tardar minutos en reflejar el icono actualizado, o no actualizarlo hasta reiniciar Explorer.
## Gotchas
- `ie4uinit.exe` debe estar en el PATH de WSL2 (normalmente via `/mnt/c/Windows/System32/`). Si Windows esta muy roto puede no encontrarse — la funcion retornara 1 con mensaje de error.
- El cambio puede tardar 1-2 segundos en propagarse visualmente despues de que la funcion retorne.
- Algunos casos extremos (icono cacheado en el dockable taskbar previamente fijado) requieren desanclar y volver a anclar el ejecutable, o reiniciar `explorer.exe`. Esta funcion no mata Explorer — seria demasiado disruptivo.
- Solo funciona desde WSL2 con acceso a herramientas Windows (`/mnt/c/Windows/System32/` en PATH). No tiene efecto en Linux nativo.
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# refresh_windows_icon_cache — Fuerza a Windows Explorer a recargar la cache
# de iconos desde WSL2. Best-effort: nunca aborta, retorna 0 si alguna
# estrategia tuvo exito.
refresh_windows_icon_cache() {
# Estrategia 1: ie4uinit.exe -show (Windows 10/11 — emite SHCNE_ASSOCCHANGED)
if command -v ie4uinit.exe >/dev/null 2>&1; then
if ie4uinit.exe -show >/dev/null 2>&1; then
echo "icon cache refresh: ok via ie4uinit -show"
return 0
fi
# Estrategia 2: ie4uinit.exe -ClearIconCache (fallback para builds viejos)
if ie4uinit.exe -ClearIconCache >/dev/null 2>&1; then
echo "icon cache refresh: ok via ie4uinit -ClearIconCache"
return 0
fi
fi
echo "icon cache refresh: failed (ie4uinit.exe not found or all strategies failed)"
return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
refresh_windows_icon_cache "$@"
fi
+14 -5
View File
@@ -3,11 +3,11 @@ name: resolve_cpp_app_dir
kind: function
lang: bash
domain: infra
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "resolve_cpp_app_dir(app_name?: string) -> stdout: app_name\tapp_dir"
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en ambas ubicaciones. Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
tags: [cpp, resolve, app, directory, infra]
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en las tres ubicaciones (apps/ canonical issue 0096 primero, luego cpp/apps/ legacy, luego projects/*/apps/). Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
tags: [cpp, resolve, app, directory, infra, cpp-windows]
uses_functions: []
uses_types: []
returns: []
@@ -20,7 +20,7 @@ test_file_path: ""
file_path: "bash/functions/infra/resolve_cpp_app_dir.sh"
params:
- name: app_name
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de cpp/apps/<X>/ o projects/*/apps/<X>/."
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/."
output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En caso de error imprime ayuda a stderr y sale con exit 1."
---
@@ -44,4 +44,13 @@ APP_DIR="$(echo "$resolved" | cut -f2)"
## Notas
Busca en orden: primero `$ROOT/cpp/apps/<X>`, luego `$ROOT/projects/*/apps/<X>` (primer match gana). Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente.
Busca en orden:
1. `$ROOT/apps/<X>` con `CMakeLists.txt` — layout canonical post-issue 0096.
2. `$ROOT/cpp/apps/<X>` — legacy pre-issue 0096.
3. `$ROOT/projects/*/apps/<X>` — apps de un proyecto (primer match gana).
Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente. Helper interno `_list_cpp_apps` evita duplicar codigo en los paths de error.
### Growth log
- v1.1.0 (2026-05-16) — busca tambien en `apps/<X>/` (canonical issue 0096). Antes solo cubria `cpp/apps/<X>/` y `projects/*/apps/<X>/`, lo que hacia que `./fn run compile_cpp_app <name>` fallara para apps movidas al layout canonical (ej. `dag_engine_ui`).
+25 -20
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# resolve_cpp_app_dir — Resuelve nombre y directorio absoluto de una app C++ del registry.
# Sin arg: deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/.
# Con arg: usa el nombre directamente y busca en ambas ubicaciones.
# Sin arg: deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/.
# Con arg: usa el nombre directamente y busca en las tres ubicaciones.
# Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0.
# Error: lista apps disponibles en stderr + exit 1.
@@ -9,18 +9,28 @@ resolve_cpp_app_dir() {
local app_arg="${1:-}"
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
_list_cpp_apps() {
ls "$root/apps/" 2>/dev/null | sed 's/^/ apps\//'
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
for proj in "$root"/projects/*/apps/; do
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
done
}
# --- Deducir desde CWD si no hay argumento ---
if [ -z "$app_arg" ]; then
local cwd
cwd="$(pwd)"
case "$cwd" in
"$root"/apps/*/|"$root"/apps/*)
local rel="${cwd#"$root/apps/"}"
app_arg="${rel%%/*}"
;;
"$root"/cpp/apps/*/|"$root"/cpp/apps/*)
# Extraer primer segmento tras cpp/apps/
local rel="${cwd#"$root/cpp/apps/"}"
app_arg="${rel%%/*}"
;;
"$root"/projects/*/apps/*/|"$root"/projects/*/apps/*)
# Extraer primer segmento tras la ultima /apps/
local rel="${cwd#"$root/projects/"}"
rel="${rel#*/apps/}"
app_arg="${rel%%/*}"
@@ -33,12 +43,7 @@ resolve_cpp_app_dir() {
echo "ERROR: no se pudo deducir la app desde el directorio actual." >&2
echo "" >&2
echo "Apps disponibles:" >&2
{
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
for proj in "$root"/projects/*/apps/; do
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
done
} >&2
_list_cpp_apps >&2
echo "" >&2
echo "Uso: resolve_cpp_app_dir <app_name>" >&2
return 1
@@ -47,12 +52,17 @@ resolve_cpp_app_dir() {
# --- Buscar directorio real ---
local app_dir=""
# Primero: cpp/apps/<X>
if [ -d "$root/cpp/apps/$app_arg" ]; then
# Primero (issue 0096 canonical): apps/<X>
if [ -d "$root/apps/$app_arg" ] && [ -f "$root/apps/$app_arg/CMakeLists.txt" ]; then
app_dir="$root/apps/$app_arg"
fi
# Segundo (legacy): cpp/apps/<X>
if [ -z "$app_dir" ] && [ -d "$root/cpp/apps/$app_arg" ]; then
app_dir="$root/cpp/apps/$app_arg"
fi
# Segundo: projects/*/apps/<X> (primer match)
# Tercero: projects/*/apps/<X> (primer match)
if [ -z "$app_dir" ]; then
for cand in "$root"/projects/*/apps/"$app_arg"; do
if [ -d "$cand" ]; then
@@ -63,15 +73,10 @@ resolve_cpp_app_dir() {
fi
if [ -z "$app_dir" ]; then
echo "ERROR: no se encuentra app '$app_arg' en cpp/apps/ ni en projects/*/apps/" >&2
echo "ERROR: no se encuentra app '$app_arg' en apps/, cpp/apps/ ni en projects/*/apps/" >&2
echo "" >&2
echo "Apps disponibles:" >&2
{
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
for proj in "$root"/projects/*/apps/; do
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
done
} >&2
_list_cpp_apps >&2
return 1
fi
@@ -0,0 +1,56 @@
---
name: fn_sync_with_pass
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "fn_sync_with_pass [status|locations|<args>...]"
description: "Wrapper de fn sync que lee credenciales del password-store pass y exporta FN_REGISTRY_API y REGISTRY_API_TOKEN antes de invocar el CLI. Evita persistir secretos en ~/.zshrc."
tags: [sync, registry, pass, gpg, launcher]
uses_functions:
- pass_get_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "registry/basicauth-user"
desc: "Entry de pass con el usuario para basicAuth del registry API (linea 1)"
- name: "registry/basicauth-pass"
desc: "Entry de pass con la contraseña para basicAuth del registry API (linea 1)"
- name: "registry/api-token"
desc: "Entry de pass con el REGISTRY_API_TOKEN (linea 1)"
- name: "args"
desc: "Argumentos opcionales forwarded a fn sync: status, locations, o nada para push+pull completo"
output: "Mismo output que ./fn sync (stdin/stdout/stderr heredados). Exit code del subproceso fn sync."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/fn_sync_with_pass.sh"
---
## Ejemplo
```bash
# Sync simple (push+pull completo)
./fn run fn_sync_with_pass_bash_pipelines
# Ver estado local: PC, API, conteos
./fn run fn_sync_with_pass_bash_pipelines status
# Mapa de ubicaciones cross-PC
./fn run fn_sync_with_pass_bash_pipelines locations
```
## Cuando usarla
Cuando necesites ejecutar `fn sync` sin tener las credenciales exportadas en el entorno. Sustituye al bloque de `export FN_REGISTRY_API=...` que de otro modo habria que poner en `~/.zshrc`.
## Gotchas
- Si GPG no tiene la clave desbloqueada, `pass show` abre el prompt del agente gpg. Dejarlo pasar — no capturar stderr para no interferir con el pinentry.
- Requiere que el password-store este inicializado (`pass init`). Si no existe, `pass show` falla con error claro.
- `FN_REGISTRY_ROOT` debe apuntar a la raiz del registry donde vive el binario `./fn`. Si no esta seteado, se resuelve via `git rev-parse --show-toplevel`.
- Los tres entries de pass deben tener el valor en la **linea 1** (convencion estandar de pass). Metadata adicional en lineas siguientes es ignorada.
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# fn_sync_with_pass — Wrapper de fn sync que lee credenciales desde pass.
set -euo pipefail
FN_ROOT="${FN_REGISTRY_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
fn_sync_with_pass() {
command -v pass >/dev/null 2>&1 || {
echo "fn_sync_with_pass: 'pass' CLI no instalado. Instala con: apt install pass" >&2
return 127
}
local u p t
u=$(pass show registry/basicauth-user 2>/dev/null | head -n1) || {
echo "fn_sync_with_pass: falta registry/basicauth-user en pass. Crea con: pass insert registry/basicauth-user" >&2
return 1
}
p=$(pass show registry/basicauth-pass 2>/dev/null | head -n1) || {
echo "fn_sync_with_pass: falta registry/basicauth-pass en pass. Crea con: pass insert registry/basicauth-pass" >&2
return 1
}
t=$(pass show registry/api-token 2>/dev/null | head -n1) || {
echo "fn_sync_with_pass: falta registry/api-token en pass. Crea con: pass insert registry/api-token" >&2
return 1
}
export FN_REGISTRY_API="https://${u}:${p}@registry.organic-machine.com"
export REGISTRY_API_TOKEN="$t"
cd "$FN_ROOT"
./fn sync "$@"
}
# Ejecucion directa (no library mode)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
fn_sync_with_pass "$@"
fi
+29 -7
View File
@@ -3,11 +3,11 @@ name: init_cpp_app
kind: pipeline
lang: bash
domain: pipelines
version: "0.1.0"
version: "1.1.0"
purity: impure
signature: "init_cpp_app(name: string, [--project <p>] [--domain <d>] [--desc <s>] [--tags <csv>]) -> void"
description: "Scaffolder estandar de apps C++ del registry. Genera main.cpp + CMakeLists.txt + app.md siguiendo el patron canonico (cfg.about/log/panels, sin app_menubar manual, dockspace via framework), registra la app en cpp/CMakeLists.txt, inicializa repo Gitea dataforge/<name> y ejecuta fn index."
tags: [cpp, imgui, scaffold, pipeline, bash, launcher]
tags: [cpp, imgui, scaffold, pipeline, bash, launcher, cpp-tables]
uses_functions:
- ensure_repo_synced_bash_infra
uses_types: []
@@ -47,9 +47,9 @@ fn run init_cpp_app finance_panel --project budget --desc "Panel de finanzas" --
```
<dir>/
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render)
CMakeLists.txt # add_imgui_app(<name> main.cpp)
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url)
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render) + data_table comentado
CMakeLists.txt # add_imgui_app(<name> main.cpp) + target_link_libraries fn_table_viz (con guard)
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url) + uses_functions comentados
```
Y ademas:
@@ -66,9 +66,31 @@ La plantilla cumple `cpp/PATTERNS.md`:
- NO llama `DockSpaceOverViewport` (auto_dockspace=true por defecto).
- Declara `panels[]` con un panel "Main" toggleable.
- Setea `cfg.about` (window About) y `cfg.log` (logger + ventana Logs).
- Include `viz/data_table.h` comentado + panel "Data" comentado en `render()` — descomentar para activar `data_table::render()`.
## Activar data_table::render()
1. En `main.cpp`: descomentar `#include "viz/data_table.h"` y el bloque del panel Data en `render()`.
2. En `app.md`: descomentar los 12 IDs del stack `fn_table_viz` en `uses_functions`.
3. El `CMakeLists.txt` ya linka `fn_table_viz` via guard `if(TARGET fn_table_viz)` — sin cambio manual.
4. Poblar `data_tables` con tus `data_table::TableInput` y el panel aparece en el DockSpace.
## Cuando usarla
Cuando necesites crear una app C++ nueva que siga el patron canonico del registry. Es el unico camino autorizado para crear apps en `cpp/apps/` o `projects/*/apps/` — nunca escribir `main.cpp` + `CMakeLists.txt` a mano.
## Gotchas
- Si `GITEA_URL`/`GITEA_TOKEN` no estan seteados, solo hace `git init` local (no crea repo remoto).
- `fn_table_viz` requiere que `vendor/lua` este presente en `cpp/`; el guard `if(TARGET fn_table_viz)` evita errores de link si no esta disponible.
- El bloque de `uses_functions` en `app.md` queda comentado intencionalmente — descomenta solo las funciones que la app realmente use para mantener el grafo de dependencias limpio.
## Despues de crear
1. Editar `app.md` y completar `uses_functions` cuando la app consuma funciones del registry.
2. Anadir las funciones del registry al `CMakeLists.txt` como paths absolutos: `${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp`.
1. Si usas `data_table::render()`: descomentar include + panel en `main.cpp`, descomentar IDs en `app.md`, ejecutar `fn index`.
2. Para otras funciones del registry: anadir paths absolutos en `CMakeLists.txt` y los IDs en `uses_functions` de `app.md`.
3. Build: `cd cpp && cmake --build build --target <name> -j`.
## Capability growth log
v1.1.0 (2026-05-15) — Auto-wires fn_table_viz; new apps get target_link_libraries + commented data_table template.
+36 -7
View File
@@ -8,7 +8,7 @@
# Uso:
# init_cpp_app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"]
#
# Por defecto domain=tools, sin proyecto (cpp/apps/<name>/).
# Por defecto domain=tools, sin proyecto (apps/<name>/, issue 0096).
set -euo pipefail
@@ -55,7 +55,7 @@ init_cpp_app() {
fi
rel_dir="projects/$project/apps/$name"
else
rel_dir="cpp/apps/$name"
rel_dir="apps/$name"
fi
abs_dir="$FN_ROOT/$rel_dir"
@@ -69,9 +69,11 @@ init_cpp_app() {
# ---------- main.cpp ----------
cat > "$abs_dir/main.cpp" <<EOF
#include <imgui.h>
#include "framework/app_base.h"
#include "app_base.h"
#include "core/panel_menu.h"
#include "core/icons_tabler.h"
#include "core/logger.h"
// #include "viz/data_table.h" // uncomment to enable data_table::render() panel
// Toggles de paneles (visibles desde el menu View del menubar canonico)
static bool g_show_main = true;
@@ -90,6 +92,11 @@ static void render() {
// DockSpaceOverViewport central (auto_dockspace=true por defecto).
// Aqui solo se dibujan los paneles propios de la app.
if (g_show_main) draw_main();
// === Data panel (uncomment to enable) ===
// static data_table::State data_state;
// static std::vector<data_table::TableInput> data_tables; // populate from your source
// data_table::render("main_data", data_tables, data_state);
}
int main(int /*argc*/, char** /*argv*/) {
@@ -115,6 +122,12 @@ add_imgui_app($name
)
target_include_directories($name PRIVATE \${CMAKE_CURRENT_SOURCE_DIR})
# fn_table_viz: provides data_table::render(), viz_render, TQL engine, Lua, LLM.
# Guard keeps the app compilable in builds where vendor/lua is absent.
if(TARGET fn_table_viz)
target_link_libraries($name PRIVATE fn_table_viz)
endif()
if(WIN32)
set_target_properties($name PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
@@ -135,7 +148,20 @@ lang: cpp
domain: $domain
description: "$desc"
tags: $tags_yaml
uses_functions: []
uses_functions:
# Uncomment when using data_table::render() — provided via fn_table_viz:
# - data_table_cpp_viz
# - viz_render_cpp_viz
# - compute_stage_cpp_core
# - compute_pipeline_cpp_core
# - compute_column_stats_cpp_core
# - auto_detect_type_cpp_core
# - tql_emit_cpp_core
# - tql_apply_cpp_core
# - lua_engine_cpp_core
# - join_tables_cpp_core
# - tql_to_sql_cpp_core
# - llm_anthropic_cpp_core
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
@@ -175,11 +201,14 @@ if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
endif()
EOF
else
local upper
upper="$(echo "$name" | tr '[:lower:]' '[:upper:]')"
cat >> "$cpp_cmake" <<EOF
# --- $name ---
if(EXISTS \${CMAKE_CURRENT_SOURCE_DIR}/apps/$name/CMakeLists.txt)
add_subdirectory(apps/$name)
# --- $name (lives in apps/, issue 0096) ---
set(_${upper}_DIR \${CMAKE_SOURCE_DIR}/../apps/$name)
if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
add_subdirectory(\${_${upper}_DIR} \${CMAKE_BINARY_DIR}/apps/$name)
endif()
EOF
fi
@@ -0,0 +1,92 @@
---
name: redeploy_all_cpp_apps
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "redeploy_all_cpp_apps(filter?: string) -> void"
description: "Cross-compila TODOS los apps C++ del registry en un solo cmake pass y despliega cada .exe al Desktop de Windows. Mas rapido que N builds individuales. Acepta filtro de nombre para despliegue parcial."
tags: [cpp, windows, deploy, redeploy, bulk, cpp-windows]
uses_functions:
- build_cpp_windows_bash_infra
- deploy_cpp_exe_to_windows_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/redeploy_all_cpp_apps.sh"
params:
- name: filter
desc: "Opcional. Substring para limitar el deploy a apps cuyo nombre lo contenga (ej: 'graph' solo despliega apps con 'graph' en el nombre). Sin valor = todas las apps."
output: "Imprime tabla resumen con OK/SKIPPED/FAILED y nombres de cada app. Exit 1 si al menos una app fallo el deploy."
---
## Ejemplo
```bash
# Recompilar y redesplegar TODAS las apps C++ tras un cambio en cpp/framework/
./fn run redeploy_all_cpp_apps
# Solo apps cuyo nombre contenga "graph"
./fn run redeploy_all_cpp_apps graph
```
## Cuando usarla
Tras un cambio en `cpp/framework/app_base.cpp`, `cpp/functions/core/*` o cualquier
funcion linkada a multiples apps. Ahorra correr `redeploy_cpp_app_windows <name> <dir>`
N veces — un solo cmake pass compila todo el arbol en paralelo.
## Comportamiento
1. **Build**: invoca `build_cpp_windows` sin argumento (compila todo el arbol con
`-j$(nproc)`). Un solo cmake pass — mucho mas rapido que N builds individuales.
2. **Descubrimiento**: itera `apps/*/CMakeLists.txt` y `projects/*/apps/*/CMakeLists.txt`.
**No** usa `cpp/apps/` (deprecado tras issue 0096).
3. **Filtro** (opcional): si se paso un argumento, solo procesa apps cuyo `basename`
contiene el substring.
4. **Por cada app**:
- Localiza `.exe` en `cpp/build/windows/apps/<name>/<name>.exe`; si no existe,
busca bajo `cpp/build/windows/` como fallback.
- Si no hay `.exe`: log SKIP, continua (no aborta — apps headless o sub-repos no
clonados no tienen build target).
- `taskkill.exe /IM <name>.exe /F` silencioso (no aborta si falla).
- `deploy_cpp_exe_to_windows <name> <app_dir>` (copia exe + DLLs + assets +
enrichers + runtime, preserva `local_files/`).
- Error por app: log FAILED, continua con la siguiente.
5. **Resumen final**: tabla `OK / SKIPPED / FAILED` con nombres. Exit 1 si hay
al menos un FAILED.
## Variables de entorno
| Variable | Default | Descripcion |
|---|---|---|
| `FN_REGISTRY_ROOT` | auto-detect | Raiz del registry (busca hacia arriba desde el script) |
| `BUILD_WIN` | `$root/cpp/build/windows` | Directorio de build Windows |
| `WIN_DESKTOP_APPS` | `/mnt/c/Users/lucas/Desktop/apps` | Destino de deploy en Windows |
## Gotchas
- Solo Windows (cross-compile mingw-w64 + Desktop deploy via WSL2). En Linux puro no aplica.
- `taskkill.exe` requiere WSL2 con interop habilitado. No funciona en WSL1 ni Linux nativo.
- Algunas apps pueden no estar en el grafo cmake actual (sub-repo no clonado, `add_subdirectory`
protegido por `if(EXISTS ...)`). El pipeline las SKIPea sin abortar — comportamiento esperado.
- Build paralelo puede consumir varios GB de RAM. Si hay OOM, reducir paralelismo exportando
`BUILD_JOBS=4` antes de invocar (actualmente la funcion `build_cpp_windows` usa `$(nproc)`;
si necesitas override edita `BUILD_JOBS` como variable de entorno custom o fork la funcion).
- El loop de deploy atrapa errores por app (`|| { failed+=...; continue; }`) para no abortar
en el primer fallo — todas las apps se intentan aunque alguna falle.
## Capability growth log
- v1.0.0 (2026-05-16) — creacion. Tras issue 0096 (apps movidas a `apps/<X>/`) el patron "recompilar+desplegar todas tras un cambio en `cpp/framework/`" se repitio varias veces sin un wrapper. Pipeline tolerante a fallos: build best-effort (test_* roto en mingw no aborta), deploy por app captura fallos individuales, summary OK/SKIPPED/FAILED al final. Primera corrida real (16 May 2026): 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED.
## Notas operativas (2026-05-16)
- `build_cpp_windows` sin arg compila el arbol entero. Si hay targets rotos (ej. `test_llm_anthropic`, `test_graph_icons` usan `setenv()` no disponible en mingw-w64), el pipeline logea `[1/2] Build returned exit=N — continuing with deploy of available exes` y sigue con la fase de deploy. Cada app sin `.exe` queda SKIPPED.
- Tras una corrida exitosa, los `.exe` quedan en `/mnt/c/Users/lucas/Desktop/apps/<name>/<name>.exe`. Lanzar individualmente con `./fn run is_cpp_app_running_windows <name>` para chequear y `launch_cpp_app_windows <name>` para arrancar.
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# redeploy_all_cpp_apps — Cross-compila TODOS los apps C++ del registry en un solo
# cmake pass y despliega cada .exe al Desktop de Windows.
# Uso: redeploy_all_cpp_apps [filter]
# filter substring opcional para limitar el deploy a apps cuyo nombre lo contenga
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../infra/build_cpp_windows.sh"
source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh"
redeploy_all_cpp_apps() {
local filter="${1:-}"
# --- Localizar raiz del registry ---
local root="${FN_REGISTRY_ROOT:-}"
if [ -z "$root" ]; then
local d="$SCRIPT_DIR"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ] && [ -d "$d/cpp" ]; then
root="$d"; break
fi
d="$(dirname "$d")"
done
fi
if [ -z "$root" ]; then
echo "[redeploy_all_cpp_apps] ERROR: no se localiza la raiz del registry. Exporta FN_REGISTRY_ROOT." >&2
return 2
fi
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
# --- Paso 1: compilar TODO el arbol (un solo cmake pass) ---
# Tolerante a fallos: si algun target (ej. test_* roto en mingw, app con
# bug puntual) falla, los demas exes que SI se construyeron siguen siendo
# desplegables. El loop de deploy hace SKIP por cada app sin .exe, asi que
# el modo "build best-effort + deploy lo que haya" es seguro.
echo "[1/2] Cross-compiling all C++ targets (best-effort)..."
local build_rc=0
build_cpp_windows || build_rc=$?
if [ "$build_rc" -ne 0 ]; then
echo "[1/2] Build returned exit=$build_rc — continuing with deploy of available exes" >&2
else
echo "[1/2] Build OK"
fi
# --- Descubrir apps con CMakeLists.txt ---
# Busca en apps/*/ y projects/*/apps/*/ (no en cpp/apps/ — deprecado)
local -a app_dirs=()
while IFS= read -r cmakelists; do
app_dirs+=("$(dirname "$cmakelists")")
done < <(
find "$root/apps" -maxdepth 2 -name "CMakeLists.txt" 2>/dev/null | sort
find "$root/projects" -maxdepth 4 -path "*/apps/*/CMakeLists.txt" 2>/dev/null | sort
)
if [ ${#app_dirs[@]} -eq 0 ]; then
echo "[redeploy_all_cpp_apps] WARN: no se encontraron apps con CMakeLists.txt" >&2
return 0
fi
# --- Paso 2: deploy por app ---
echo "[2/2] Deploying apps to Windows Desktop..."
local -a ok=() skipped=() failed=()
for app_dir in "${app_dirs[@]}"; do
local name
name="$(basename "$app_dir")"
# Aplicar filtro si se indico
if [ -n "$filter" ] && [[ "$name" != *"$filter"* ]]; then
continue
fi
# Localizar el .exe en la ubicacion canonica
local exe_path="$build_win/apps/$name/$name.exe"
if [ ! -f "$exe_path" ]; then
# Fallback: buscar bajo build_win/
exe_path="$(find "$build_win" -name "$name.exe" -type f 2>/dev/null | head -n1 || true)"
fi
if [ -z "$exe_path" ] || [ ! -f "$exe_path" ]; then
echo " SKIP: $name — .exe no encontrado en $build_win" >&2
skipped+=("$name")
continue
fi
# taskkill silencioso (pre-autorizado; deploy_cpp_exe_to_windows lo hace internamente,
# pero si deploy falla antes de llegar ahi nos aseguramos de liberar el lock)
if command -v taskkill.exe >/dev/null 2>&1; then
taskkill.exe /IM "${name}.exe" /F >/dev/null 2>&1 || true
fi
if deploy_cpp_exe_to_windows "$name" "$app_dir"; then
ok+=("$name")
else
echo " FAILED: $name" >&2
failed+=("$name")
fi
done
# --- Resumen ---
echo ""
echo "===== redeploy_all_cpp_apps — summary ====="
printf " OK : %d\n" "${#ok[@]}"
printf " SKIPPED : %d\n" "${#skipped[@]}"
printf " FAILED : %d\n" "${#failed[@]}"
if [ ${#ok[@]} -gt 0 ]; then
echo " Deployed:"
for n in "${ok[@]}"; do printf " + %s\n" "$n"; done
fi
if [ ${#skipped[@]} -gt 0 ]; then
echo " Skipped (no .exe):"
for n in "${skipped[@]}"; do printf " - %s\n" "$n"; done
fi
if [ ${#failed[@]} -gt 0 ]; then
echo " Failed:"
for n in "${failed[@]}"; do printf " x %s\n" "$n"; done
return 1
fi
}
# Ejecutar si se llama directamente (fn run lo invoca como script)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
redeploy_all_cpp_apps "$@"
fi
@@ -3,16 +3,17 @@ name: redeploy_cpp_app_windows
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "redeploy_cpp_app_windows(app_name: string, app_dir: string, [--build]) -> void"
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify."
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify e incluye refresh del icon cache del shell."
tags: [cpp, windows, redeploy, pipeline, wsl, launcher, cpp-windows]
uses_functions:
- build_cpp_windows_bash_infra
- deploy_cpp_exe_to_windows_bash_infra
- launch_cpp_app_windows_bash_infra
- is_cpp_app_running_windows_bash_infra
- refresh_windows_icon_cache_bash_infra
uses_types: []
returns: []
returns_optional: false
@@ -47,6 +48,7 @@ redeploy_cpp_app_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_de
1. **Parsear flag `--build`** (default off, opt-in).
2. **Si `--build`**: invocar `build_cpp_windows <app_name>` para compilar `cpp/build/windows/apps/<app_name>/<app_name>.exe`. Si falla, exit 1 sin tocar el Desktop.
3. **Deploy**: invocar `deploy_cpp_exe_to_windows "<app_name>" "<app_dir>"`. Esta función mata el proceso si está vivo (taskkill.exe pre-autorizado), copia exe + DLLs + assets + runtime + enrichers, y preserva `local_files/`.
3b. **Refresh icon cache** (v1.1.0+): invocar `refresh_windows_icon_cache` (best-effort). Llama `ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar al timestamp. Si falla, no aborta el pipeline.
4. **Launch**: invocar `launch_cpp_app_windows "<app_name>"` para arrancar la app en Windows.
5. **Wait**: `sleep 1` — espera arranque corto.
6. **Verify**: invocar `is_cpp_app_running_windows "<app_name>"`. Si NO está vivo → exit 1 con mensaje claro.
@@ -7,6 +7,7 @@ source "$SCRIPT_DIR/../infra/build_cpp_windows.sh"
source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh"
source "$SCRIPT_DIR/../infra/launch_cpp_app_windows.sh"
source "$SCRIPT_DIR/../infra/is_cpp_app_running_windows.sh"
source "$SCRIPT_DIR/../infra/refresh_windows_icon_cache.sh"
redeploy_cpp_app_windows() {
local app_name=""
@@ -63,6 +64,12 @@ redeploy_cpp_app_windows() {
fi
echo "[2/4] Deploy OK"
# Refrescar cache de iconos del shell. Sin esto el .exe nuevo puede salir
# con el icono generico (Windows cachea por timestamp/path en iconcache.db
# y a veces no detecta el cambio inmediatamente). Best-effort: si falla
# no abortamos el redeploy.
refresh_windows_icon_cache || true
# Paso 3: lanzar la app
echo "[3/4] Launching $app_name..."
if ! launch_cpp_app_windows "$app_name"; then
@@ -0,0 +1,83 @@
---
name: refresh_app_hub
kind: pipeline
lang: bash
domain: pipelines
version: "1.1.0"
purity: impure
signature: "refresh_app_hub([--hub-dir <path>] [--size <px>] [--no-restart] [--style <s>]) -> void"
description: "Pipeline orquestador que regenera los iconos PNG y el manifest TSV del App Hub desde registry.db y reinicia el proceso app_hub_launcher en Windows. Cubre el ciclo completo: export icons → export manifest → taskkill → relaunch. Default style=white_duotone (duotone Phosphor blanco sobre bg accent). Override con --style fill_white | adaptive_duotone."
tags: [hub, launcher, icons, manifest, cpp, windows, wsl, cpp-windows, deploy]
uses_functions:
- export_hub_icons_py_infra
- export_hub_manifest_py_infra
- is_cpp_app_running_windows_bash_infra
- launch_cpp_app_windows_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/refresh_app_hub.sh"
params:
- name: "--hub-dir"
desc: "Directorio local_files del hub en Windows (accesible desde WSL). Default: /mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files. Los PNGs se escriben en <hub-dir>/icons/ y el manifest en <hub-dir>/hub_manifest.tsv."
- name: "--size"
desc: "Lado en pixels de cada PNG de icono. Default 64. Pasar 128 para pantallas HiDPI."
- name: "--no-restart"
desc: "Si está presente, solo regenera icons + manifest sin tocar el proceso del hub (pasos 3 y 4 se marcan como skipped). Útil para actualizar los archivos mientras el hub está cerrado manualmente."
output: "Imprime resumen de 4 pasos (icons exported, manifest rows, hub killed/skipped, hub launched) y finaliza con 'OK: app_hub refreshed'. Exit 1 si falla cualquier paso, con mensaje indicando cuál."
---
## Ejemplo
```bash
# Caso habitual: regenerar todo y reiniciar el hub
./fn run refresh_app_hub
# Solo regenerar iconos y manifest, sin tocar el proceso
./fn run refresh_app_hub --no-restart
# Iconos a 128px para pantalla HiDPI o hub-dir personalizado
./fn run refresh_app_hub --size 128
./fn run refresh_app_hub --hub-dir /mnt/d/MiDesktop/apps/app_hub_launcher/local_files
# Combinado
./fn run refresh_app_hub --size 128 --hub-dir /mnt/d/MiDesktop/apps/app_hub_launcher/local_files
```
Salida esperada:
```
[1/4] Exporting PNG icons (size=64px) → /mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files/icons ...
[1/4] PNG icons exported: 12
[2/4] Exporting manifest → .../hub_manifest.tsv ...
[2/4] Manifest exported: 12 rows
[3/4] Hub running → killing app_hub_launcher.exe ...
[3/4] Hub running → killed
[4/4] Launching app_hub_launcher ...
[4/4] Hub launched
OK: app_hub refreshed
```
## Cuando usarla
Ejecutar este pipeline después de:
- Cambiar el bloque `icon:` (`phosphor`, `accent`) en cualquier `app.md` C++.
- Modificar la `description` de una app C++ (se refleja en el manifest TSV).
- Añadir una app nueva con `lang: cpp` y `framework: imgui` al registry.
- Cambiar el `name` de una app imgui (el PNG y la fila TSV usan el `name`).
En todos esos casos el hub cachea los archivos al arrancar, así que el reinicio es obligatorio para ver los cambios.
## Gotchas
- **Cache al arrancar**: `app_hub_launcher` carga `local_files/icons/*.png` y `local_files/hub_manifest.tsv` una sola vez al inicio. Sin el reinicio los cambios no se ven aunque los archivos estén actualizados. Usar `--no-restart` solo si el hub está cerrado o si quieres preparar los archivos antes de lanzarlo a mano.
- **Paths Windows requieren WSL `/mnt/c/`**: el `--hub-dir` debe ser una ruta accesible desde WSL2. Las rutas `C:\...` nativas no funcionan en Bash — convertirlas con `wslpath -u 'C:\...'` antes de pasar el flag.
- **`taskkill` solo funciona en WSL con acceso a Windows tools**: si `tasklist.exe` y `taskkill.exe` no están en `$PATH` (instalación WSL sin interop habilitado), el paso 3 fallará. Verificar con `command -v tasklist.exe`.
- **`powershell.exe` necesario para el lanzamiento**: `launch_cpp_app_windows` usa `Start-Process` de PowerShell. Si PowerShell no está en `$PATH`, el paso 4 fallará.
- **`export_hub_icons` requiere Phosphor SVGs**: los SVGs deben existir en `sources/phosphor-core/assets/fill/`. Si no están clonados, las apps sin SVG se omiten (skip) sin abortar; el count final puede ser menor al esperado. Clonar con: `git clone https://github.com/phosphor-icons/core.git sources/phosphor-core`.
- **Idempotente**: lanzable N veces. Si el hub no está corriendo, el paso 3 se salta y el paso 4 lo lanza igualmente. Si ya está corriendo, lo mata y lo relanza.
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# refresh_app_hub — Pipeline: regenera icons + manifest del App Hub y reinicia el proceso
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$SCRIPT_DIR/../infra/is_cpp_app_running_windows.sh"
source "$SCRIPT_DIR/../infra/launch_cpp_app_windows.sh"
PYTHON="${REGISTRY_ROOT}/python/.venv/bin/python3"
HUB_APP="app_hub_launcher"
refresh_app_hub() {
local hub_dir="/mnt/c/Users/lucas/Desktop/apps/app_hub_launcher/local_files"
local size=64
local no_restart=0
local style="white_duotone"
# Parsear flags
while [[ $# -gt 0 ]]; do
case "$1" in
--hub-dir)
hub_dir="$2"
shift 2
;;
--size)
size="$2"
shift 2
;;
--no-restart)
no_restart=1
shift
;;
--style)
style="$2"
shift 2
;;
-*)
echo "refresh_app_hub: flag desconocido: $1" >&2
return 1
;;
*)
echo "refresh_app_hub: argumento inesperado: $1" >&2
return 1
;;
esac
done
local icons_dir="${hub_dir}/icons"
local manifest_path="${hub_dir}/hub_manifest.tsv"
# Paso 1: exportar PNGs de iconos
echo "[1/4] Exporting PNG icons (size=${size}px) → ${icons_dir} ..."
local icons_json
icons_json=$(
PYTHONPATH="${REGISTRY_ROOT}/python/functions" \
FN_REGISTRY_ROOT="${REGISTRY_ROOT}" \
"$PYTHON" \
"${REGISTRY_ROOT}/python/functions/infra/export_hub_icons.py" \
"$icons_dir" \
--size "$size" \
--registry-root "$REGISTRY_ROOT" \
--style "$style"
) || {
echo "ERROR [1/4]: export_hub_icons falló" >&2
return 1
}
local icon_count
icon_count=$(echo "$icons_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])" 2>/dev/null || echo "?")
echo "[1/4] PNG icons exported: ${icon_count}"
# Paso 2: exportar TSV manifest
echo "[2/4] Exporting manifest → ${manifest_path} ..."
local manifest_json
manifest_json=$(
PYTHONPATH="${REGISTRY_ROOT}/python/functions" \
FN_REGISTRY_ROOT="${REGISTRY_ROOT}" \
"$PYTHON" \
"${REGISTRY_ROOT}/python/functions/infra/export_hub_manifest.py" \
"$manifest_path" \
--registry-root "$REGISTRY_ROOT"
) || {
echo "ERROR [2/4]: export_hub_manifest falló" >&2
return 1
}
local manifest_count
manifest_count=$(echo "$manifest_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['count'])" 2>/dev/null || echo "?")
echo "[2/4] Manifest exported: ${manifest_count} rows"
# Si --no-restart, terminar aqui
if [[ $no_restart -eq 1 ]]; then
echo "[3/4] Kill skipped (--no-restart)"
echo "[4/4] Launch skipped (--no-restart)"
echo "OK: app_hub refreshed (no-restart)"
return 0
fi
# Paso 3: matar el hub si está corriendo
local running=0
if is_cpp_app_running_windows "$HUB_APP" >/dev/null 2>&1; then
running=1
fi
if [[ $running -eq 1 ]]; then
echo "[3/4] Hub running → killing ${HUB_APP}.exe ..."
taskkill.exe /IM "${HUB_APP}.exe" /F >/dev/null 2>&1 || {
echo "ERROR [3/4]: taskkill falló para ${HUB_APP}.exe" >&2
return 1
}
# Pequeña pausa para que Windows libere el handle antes del relanzamiento
sleep 1
echo "[3/4] Hub running → killed"
else
echo "[3/4] Hub not running → skip kill"
fi
# Paso 4: relanzar el hub
echo "[4/4] Launching ${HUB_APP} ..."
if ! launch_cpp_app_windows "$HUB_APP"; then
echo "ERROR [4/4]: launch_cpp_app_windows falló para '${HUB_APP}'" >&2
return 1
fi
echo "[4/4] Hub launched"
echo "OK: app_hub refreshed"
}
# Ejecutar si se llama directamente (fn run lo invoca como script)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
refresh_app_hub "$@"
fi
+255 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
@@ -39,6 +40,8 @@ func cmdDoctor(args []string) {
doctorArtefacts(r, jsonOut)
case "services":
doctorServices(r, jsonOut)
case "services-spec":
doctorServicesSpec(r, jsonOut)
case "sync":
doctorSync(r, jsonOut)
case "uses-functions":
@@ -59,6 +62,14 @@ func cmdDoctor(args []string) {
} else {
doctorCapabilities(r, jsonOut)
}
case "app-location":
doctorAppLocation(r, jsonOut)
case "modules":
doctorModules(r, jsonOut)
case "dod":
doctorDod(r, jsonOut)
case "e2e-coverage":
doctorE2ECoverage(r, jsonOut)
default:
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
doctorUsage()
@@ -76,6 +87,7 @@ Subcommands:
(none)|all Corre todos los checks
artefacts Salud de apps y analyses (git, venv, app.md, upstream)
services Estado de apps con tag 'service' (systemd + puerto)
services-spec Audit del bloque service: en app.md de apps tag 'service' (issue 0105)
sync Drift entre pc_locations BD y disco
uses-functions Audit imports reales vs uses_functions del app.md
unused Funciones del registry sin consumidores
@@ -84,6 +96,10 @@ Subcommands:
vaults Salud de vaults: directorio, layout, índice, staleness, drift
copied-code Detecta cuerpos de funcion del registry copiados en apps sin import (issue 0085k)
capabilities Drift entre docs/capabilities/INDEX.md, tags de funciones, y paginas <grupo>.md (issue 0086)
app-location Detecta artefactos (apps/analysis) en carpetas de lenguaje (cpp/apps/, etc.) - issue 0096
modules Drift entre uses_modules (app.md) y fn_module_<x> link calls (CMakeLists.txt) - issue 0097
dod Audita bloque dod_evidence_schema en dev/issues/ y dev/flows/ (issue 0114)
e2e-coverage Porcentaje de apps con e2e_checks declarado en su app.md (issue 0121b)
Flags:
--json Salida JSON (para scripting/agentes)
@@ -123,6 +139,11 @@ func doctorAll(root string, jsonOut bool) {
} else {
all["cpp_apps_error"] = err.Error()
}
if v, err := infra.AuditCppTableMigration(root); err == nil {
all["cpp_table_migration"] = v
} else {
all["cpp_table_migration_error"] = err.Error()
}
if v, err := infra.AuditMlEnv(root); err == nil {
all["ml"] = v
} else {
@@ -168,10 +189,21 @@ func doctorCppApps(root string, jsonOut bool) {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
tableAudits, err2 := infra.AuditCppTableMigration(root)
if err2 != nil {
fmt.Fprintf(os.Stderr, "warning: table migration audit failed: %v\n", err2)
tableAudits = nil
}
if jsonOut {
emit(audits)
emit(map[string]any{
"conformance": audits,
"table_migration": tableAudits,
})
return
}
// Conformance section.
bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tISSUES")
@@ -187,6 +219,31 @@ func doctorCppApps(root string, jsonOut bool) {
}
w.Flush()
fmt.Printf("\n%d/%d C++ apps conform.\n", len(audits)-bad, len(audits))
// BeginTable migration section.
if len(tableAudits) == 0 {
return
}
hasMigrationNotes := false
for _, t := range tableAudits {
if t.Status != "clean" {
hasMigrationNotes = true
break
}
}
if !hasMigrationNotes {
return
}
fmt.Println("\n--- BeginTable migration (issue 0081) ---")
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "STATUS\tAPP\tTABLES\tMESSAGE")
for _, t := range tableAudits {
if t.Status == "clean" {
continue
}
fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", strings.ToUpper(t.Status), t.AppID, t.BeginTableCount, t.Message)
}
tw.Flush()
}
func doctorArtefacts(root string, jsonOut bool) {
@@ -244,6 +301,58 @@ func doctorServices(root string, jsonOut bool) {
w.Flush()
}
func doctorServicesSpec(root string, jsonOut bool) {
audits, err := infra.AuditServicesSpec(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(audits)
return
}
if len(audits) == 0 {
fmt.Println("No services declared (no apps with tag 'service').")
return
}
bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tRUNTIME\tPORT\tHEALTH\tUNIT\tTARGETS\tISSUES")
for _, a := range audits {
status := "OK"
issues := "-"
if !a.OK {
status = "FAIL"
issues = strings.Join(a.Issues, "; ")
bad++
}
port := "-"
if a.Port > 0 {
port = fmt.Sprintf("%d", a.Port)
}
health := a.HealthPath
if health == "" {
health = "-"
}
unit := a.SystemdUnit
if unit == "" {
unit = "-"
}
targets := strings.Join(a.PCTargets, ",")
if targets == "" {
targets = "-"
}
runtime := a.Runtime
if runtime == "" {
runtime = "-"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
status, a.Name, runtime, port, health, unit, targets, issues)
}
w.Flush()
fmt.Printf("\n%d/%d services with complete service: block.\n", len(audits)-bad, len(audits))
}
func doctorSync(root string, jsonOut bool) {
drifts, err := infra.PcLocationsDrift(root, "")
if err != nil {
@@ -472,3 +581,148 @@ func doctorCopiedCode(root string, jsonOut bool) {
w.Flush()
fmt.Printf("\n%d suspected copy match(es).\n", len(entries))
}
func doctorAppLocation(root string, jsonOut bool) {
violations, err := infra.AuditAppLocation(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(violations)
return
}
if len(violations) == 0 {
fmt.Println("OK: no artefacts under language-named folders.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "KIND\tLANG\tPATH")
for _, v := range violations {
fmt.Fprintf(w, "%s\t%s\t%s\n", v.Kind, v.Lang, v.Path)
}
w.Flush()
fmt.Printf("\n%d violation(s): move artefact to apps/<name>/ or projects/<p>/apps/<name>/ (issue 0096).\n", len(violations))
}
func doctorModules(root string, jsonOut bool) {
checks, err := infra.AuditModulesDrift(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(checks)
return
}
bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tDECLARED\tLINKED\tMISSING\tEXTRA")
for _, c := range checks {
status := "OK"
if !c.OK {
status = "DRIFT"
bad++
}
decl := strings.Join(c.Declared, ",")
if decl == "" {
decl = "-"
}
link := strings.Join(c.Linked, ",")
if link == "" {
link = "-"
}
missing := strings.Join(c.MissingLinks, ",")
if missing == "" {
missing = "-"
}
extra := strings.Join(c.ExtraLinks, ",")
if extra == "" {
extra = "-"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", status, c.AppID, decl, link, missing, extra)
}
w.Flush()
fmt.Printf("\n%d/%d apps with module drift.\n", bad, len(checks))
if bad > 0 {
fmt.Println("Fix: align uses_modules in app.md with target_link_libraries(fn_module_*) in CMakeLists.txt.")
}
}
func doctorDod(root string, jsonOut bool) {
issuesDir := filepath.Join(root, "dev", "issues")
flowsDir := filepath.Join(root, "dev", "flows")
report, err := infra.AuditDodSchema(issuesDir, flowsDir)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(report)
return
}
fmt.Println("=== DoD Schema Audit ===")
fmt.Printf("files scanned: %d\n", report.TotalFiles)
fmt.Printf("with schema: %d\n", report.FilesWithItems)
fmt.Printf("total items: %d\n", report.TotalItems)
fmt.Printf("invalid items: %d\n", report.InvalidItems)
if report.InvalidItems == 0 {
fmt.Println("\nAll DoD schemas valid.")
return
}
fmt.Println()
rel := func(p string) string {
if r, err := filepath.Rel(root, p); err == nil {
return r
}
return p
}
for _, f := range report.Files {
if len(f.Errors) == 0 {
continue
}
for _, e := range f.Errors {
fmt.Printf("%s : %s\n", rel(f.Path), e)
}
}
}
func doctorE2ECoverage(root string, jsonOut bool) {
roots := []string{
filepath.Join(root, "apps"),
filepath.Join(root, "cpp", "apps"),
filepath.Join(root, "projects"),
}
report, err := infra.AuditE2ECoverage(roots)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(report)
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "METRIC\tVALUE")
fmt.Fprintf(w, "total\t%d\n", report.Total)
fmt.Fprintf(w, "with_checks\t%d\n", report.WithChecks)
fmt.Fprintf(w, "missing\t%d\n", len(report.Missing))
fmt.Fprintf(w, "coverage_pct\t%.2f%%\n", report.CoveragePct)
w.Flush()
if len(report.Missing) > 0 {
fmt.Println("\nApps without e2e_checks:")
rel := func(p string) string {
if r, err := filepath.Rel(root, p); err == nil {
return r
}
return p
}
for _, m := range report.Missing {
fmt.Printf(" %s\n", rel(m))
}
}
}
+37 -2
View File
@@ -143,8 +143,8 @@ func cmdIndex() {
}
}
fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d unit_tests\n",
result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.UnitTests)
fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d modules, %d unit_tests\n",
result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.Modules, result.UnitTests)
for _, e := range result.ValidationErrors {
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
}
@@ -420,10 +420,42 @@ func cmdShow(args []string) {
return
}
m, errM := db.GetModule(id)
if errM == nil {
printModule(m)
return
}
fmt.Fprintf(os.Stderr, "not found: %s\n", id)
os.Exit(1)
}
func printModule(m *registry.Module) {
fmt.Printf("ID: %s\n", m.ID)
fmt.Printf("Name: %s\n", m.Name)
fmt.Printf("Version: %s\n", m.Version)
fmt.Printf("Lang: %s\n", m.Lang)
fmt.Printf("Description: %s\n", m.Description)
if len(m.Members) > 0 {
fmt.Printf("Members: %s\n", strings.Join(m.Members, ", "))
}
if len(m.Tags) > 0 {
fmt.Printf("Tags: %s\n", strings.Join(m.Tags, ", "))
}
if m.DirPath != "" {
fmt.Printf("DirPath: %s\n", m.DirPath)
}
if m.RepoURL != "" {
fmt.Printf("RepoURL: %s\n", m.RepoURL)
}
if m.Documentation != "" {
fmt.Printf("\nDocumentation:\n%s\n", m.Documentation)
}
if m.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", m.Notes)
}
}
func printFunction(f *registry.Function) {
fmt.Printf("ID: %s\n", f.ID)
fmt.Printf("Name: %s\n", f.Name)
@@ -540,6 +572,9 @@ func printApp(a *registry.App) {
if len(a.UsesTypes) > 0 {
fmt.Printf("Uses types: %s\n", strings.Join(a.UsesTypes, ", "))
}
if len(a.UsesModules) > 0 {
fmt.Printf("Uses mods: %s\n", strings.Join(a.UsesModules, ", "))
}
if a.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", a.Notes)
}
+14
View File
@@ -27,6 +27,7 @@ type syncRequest struct {
Analysis []registry.Analysis `json:"analysis"`
Projects []registry.Project `json:"projects"`
Vaults []registry.Vault `json:"vaults"`
Modules []registry.Module `json:"modules"`
Proposals []registry.Proposal `json:"proposals"`
Locations []registry.PcLocation `json:"locations"`
}
@@ -37,6 +38,7 @@ type syncResponse struct {
Analysis []registry.Analysis `json:"analysis"`
Projects []registry.Project `json:"projects"`
Vaults []registry.Vault `json:"vaults"`
Modules []registry.Module `json:"modules"`
Proposals []registry.Proposal `json:"proposals"`
Locations []registry.PcLocation `json:"locations"`
Stats struct {
@@ -100,6 +102,7 @@ func syncPushPull() {
analysis, _ := db.AllAnalysis()
projects, _ := db.ListAllProjects()
vaults, _ := db.AllVaults()
modules, _ := db.AllModules()
proposals, _ := db.AllProposals()
// 2. Scan local directories and build pc_locations
@@ -112,6 +115,7 @@ func syncPushPull() {
Analysis: analysis,
Projects: projects,
Vaults: vaults,
Modules: modules,
Proposals: proposals,
Locations: locations,
}
@@ -203,6 +207,14 @@ func applySync(db *registry.DB, resp syncResponse) int {
}
}
for _, m := range resp.Modules {
existing, err := db.GetModule(m.ID)
if err != nil || m.UpdatedAt.After(existing.UpdatedAt) {
db.InsertModule(&m)
imported++
}
}
for _, p := range resp.Proposals {
existing, err := db.GetProposal(p.ID)
if err != nil || p.UpdatedAt.After(existing.UpdatedAt) {
@@ -329,6 +341,7 @@ func syncStatus() {
analysis, _ := db.AllAnalysis()
projects, _ := db.ListAllProjects()
vaults, _ := db.AllVaults()
modules, _ := db.AllModules()
proposals, _ := db.AllProposals()
locs, _ := db.ListAllPcLocations()
@@ -337,6 +350,7 @@ func syncStatus() {
fmt.Printf(" analysis: %d\n", len(analysis))
fmt.Printf(" projects: %d\n", len(projects))
fmt.Printf(" vaults: %d\n", len(vaults))
fmt.Printf(" modules: %d\n", len(modules))
fmt.Printf(" proposals: %d\n", len(proposals))
fmt.Printf(" locations: %d\n", len(locs))
+29
View File
@@ -0,0 +1,29 @@
package main
import (
"flag"
"fmt"
"os"
"fn-registry/functions/browser"
)
func main() {
port := flag.Int("port", 9222, "CDP debug port")
headless := flag.Bool("headless", false, "headless mode")
chromePath := flag.String("chrome-path", "", "explicit chrome.exe path (optional)")
userDataDir := flag.String("user-data-dir", "", "user-data-dir (optional; WSL2 auto-translates)")
flag.Parse()
pid, err := browser.ChromeLaunch(browser.ChromeLaunchOpts{
Port: *port,
Headless: *headless,
ChromePath: *chromePath,
UserDataDir: *userDataDir,
})
if err != nil {
fmt.Fprintf(os.Stderr, "chrome_launch failed: %v\n", err)
os.Exit(1)
}
fmt.Printf("OK pid=%d port=%d\n", pid, *port)
}
+187 -31
View File
@@ -1,5 +1,9 @@
cmake_minimum_required(VERSION 3.16)
project(fn_registry_cpp LANGUAGES C CXX)
if(WIN32)
project(fn_registry_cpp LANGUAGES C CXX RC)
else()
project(fn_registry_cpp LANGUAGES C CXX)
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -236,10 +240,75 @@ endif()
set(FN_CPP_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "fn_registry cpp root")
function(add_imgui_app target)
add_executable(${target} ${ARGN})
# Windows icon: si la app tiene <app_dir>/appicon.ico, generamos un .rc
# apuntando a ese .ico y lo anadimos como fuente. mingw-w64 windres
# (CMAKE_RC_COMPILER en la toolchain) lo enlaza en el .exe.
set(_extra_sources "")
if(WIN32 AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico)
set(_rc_file ${CMAKE_CURRENT_BINARY_DIR}/${target}_appicon.rc)
# Forward slashes para que windres no se confunda con escapes.
file(TO_CMAKE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico _ico_path)
# Numeric ID 101 = FN_APP_ICON_ID (ver cpp/framework/app_base.cpp).
# Usamos ID numerico (no string "IDI_ICON1") para que LoadImageW
# pueda recuperarlo en runtime y attacharlo al HWND (WM_SETICON).
file(WRITE ${_rc_file} "101 ICON \"${_ico_path}\"\n")
list(APPEND _extra_sources ${_rc_file})
endif()
# Modules manifest (issue 0097): siempre generamos <target>_modules_generated.cpp.
# Si la app tiene app.md con uses_modules, el .cpp resultante define
# fn::app_modules_array[] con sus modulos. Si no, genera un stub vacio
# (apps sin app.md no rompen el linkage de framework's app_about).
set(_modules_gen ${CMAKE_CURRENT_BINARY_DIR}/${target}_modules_generated.cpp)
set(_codegen_script ${FN_CPP_ROOT_DIR}/../python/functions/infra/codegen_app_modules.py)
set(_modules_root ${FN_CPP_ROOT_DIR}/../modules)
set(_app_md ${CMAKE_CURRENT_SOURCE_DIR}/app.md)
if(NOT EXISTS ${_app_md})
# No app.md: emit empty stub directamente (sin invocar Python).
file(WRITE ${_modules_gen}
"// Auto-generated stub (no app.md).
#include \"app_modules.h\"
namespace fn {
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
const unsigned long app_modules_count = 0;
}
")
else()
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_FOUND AND EXISTS ${_codegen_script})
execute_process(
COMMAND ${Python3_EXECUTABLE} ${_codegen_script}
--app-md ${_app_md}
--modules-root ${_modules_root}
--app-name ${target}
--out ${_modules_gen}
RESULT_VARIABLE _codegen_rc
OUTPUT_VARIABLE _codegen_out
ERROR_VARIABLE _codegen_err
)
if(NOT _codegen_rc EQUAL 0 AND NOT _codegen_rc EQUAL 2)
message(WARNING "codegen_app_modules failed for ${target}: ${_codegen_err}")
endif()
endif()
# Si python falla o el script no esta, emit stub vacio.
if(NOT EXISTS ${_modules_gen})
file(WRITE ${_modules_gen}
"// Auto-generated stub (codegen unavailable).
#include \"app_modules.h\"
namespace fn {
const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };
const unsigned long app_modules_count = 0;
}
")
endif()
endif()
list(APPEND _extra_sources ${_modules_gen})
add_executable(${target} ${ARGN} ${_extra_sources})
target_link_libraries(${target} PRIVATE fn_framework)
target_include_directories(${target} PRIVATE
${FN_CPP_ROOT_DIR}/functions
${FN_CPP_ROOT_DIR}/framework
)
# Convencion de layout (cpp_apps.md §7):
# <exe_dir>/<app>.exe + <app>.dll (binario + DLLs Windows convention)
@@ -274,14 +343,29 @@ endfunction()
# Functions are compiled as part of apps that use them via add_imgui_app.
# Each function is a .h/.cpp pair included by the app's CMakeLists.txt.
# --- Demo app ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt)
add_subdirectory(apps/chart_demo)
# --- fn_module_data_table (issue 0097 modules) ---
# Static lib defined in modules/data_table/CMakeLists.txt. Replaces former
# fn_module_data_table target. Apps opt-in via:
# target_link_libraries(<app> PRIVATE fn_module_data_table)
# Lua is a hard dep — only build the module when the vendored lua tree exists.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
add_subdirectory(${CMAKE_SOURCE_DIR}/../modules/data_table ${CMAKE_BINARY_DIR}/modules/data_table)
endif()
# --- Shaders Lab ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt)
add_subdirectory(apps/shaders_lab)
# --- Demo app (lives in apps/, issue 0096 standardization) ---
if(NOT DEFINED _CHART_DEMO_DIR)
set(_CHART_DEMO_DIR ${CMAKE_SOURCE_DIR}/../apps/chart_demo)
endif()
if(EXISTS ${_CHART_DEMO_DIR}/CMakeLists.txt)
add_subdirectory(${_CHART_DEMO_DIR} ${CMAKE_BINARY_DIR}/apps/chart_demo)
endif()
# --- Shaders Lab (lives in apps/) ---
if(NOT DEFINED _SHADERS_LAB_DIR)
set(_SHADERS_LAB_DIR ${CMAKE_SOURCE_DIR}/../apps/shaders_lab)
endif()
if(EXISTS ${_SHADERS_LAB_DIR}/CMakeLists.txt)
add_subdirectory(${_SHADERS_LAB_DIR} ${CMAKE_BINARY_DIR}/apps/shaders_lab)
endif()
# --- Lua 5.4 vendored (para playground tables / DSL formulas) ---
@@ -289,30 +373,39 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt)
add_subdirectory(vendor/lua)
endif()
# --- Primitives Gallery (catalogo visual de primitivos core/viz/gfx) ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt)
add_subdirectory(apps/primitives_gallery)
# --- Primitives Gallery (lives in apps/) ---
if(NOT DEFINED _PG_DIR)
set(_PG_DIR ${CMAKE_SOURCE_DIR}/../apps/primitives_gallery)
endif()
if(EXISTS ${_PG_DIR}/CMakeLists.txt)
add_subdirectory(${_PG_DIR} ${CMAKE_BINARY_DIR}/apps/primitives_gallery)
endif()
# --- Tables playground (vive dentro de primitives_gallery/playground/tables/) ---
# No es un app del registry; sirve para iterar mejoras sobre table_view_cpp_viz
# antes de promover una API v2 y migrar las apps C++ que hoy usan ImGui::BeginTable raw.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/playground/tables/CMakeLists.txt)
add_subdirectory(apps/primitives_gallery/playground/tables)
# --- Tables playground DEPRECATED (issue 0108) ---
# Sustituido por apps/tables_qa. El playground legacy queda solo como historia
# del split data_table 0107c. NO se builda mas — su self_test (430 checks
# contra logica legacy) ya esta cubierto por:
# - cpp/tests/ (Catch2 unit tests de la logica pura del registry)
# - apps/tables_qa/ (testbed del modulo data_table v2.0.0+)
# Para revivirlo (temporal, debugging): descomentar el bloque if(EXISTS ...).
# if(EXISTS ${_PG_DIR}/playground/tables/CMakeLists.txt)
# add_subdirectory(${_PG_DIR}/playground/tables ${CMAKE_BINARY_DIR}/apps/primitives_gallery/playground/tables)
# endif()
# --- text_editor + file_watcher smoke test (lives in apps/) ---
if(NOT DEFINED _TES_DIR)
set(_TES_DIR ${CMAKE_SOURCE_DIR}/../apps/text_editor_smoke)
endif()
if(EXISTS ${_TES_DIR}/CMakeLists.txt)
add_subdirectory(${_TES_DIR} ${CMAKE_BINARY_DIR}/apps/text_editor_smoke)
endif()
# --- text_editor + file_watcher smoke test (issue 0025) ---
# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt)
add_subdirectory(apps/text_editor_smoke)
# --- AltSnap viewport-jitter regression test (lives in apps/) ---
if(NOT DEFINED _AJT_DIR)
set(_AJT_DIR ${CMAKE_SOURCE_DIR}/../apps/altsnap_jitter_test)
endif()
# --- AltSnap viewport-jitter regression test ---
# Headless harness que conduce glfwSetWindowPos cada frame y verifica que
# ImGui viewport->Pos sigue al OS dentro de 1px. Sin la patch del framework
# (callback GLFW + per-frame sync) este test falla exit=1.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/altsnap_jitter_test/CMakeLists.txt)
add_subdirectory(apps/altsnap_jitter_test)
if(EXISTS ${_AJT_DIR}/CMakeLists.txt)
add_subdirectory(${_AJT_DIR} ${CMAKE_BINARY_DIR}/apps/altsnap_jitter_test)
endif()
# --- gamedev stack (SDL3 + sokol_gfx + miniaudio, issue 0072) ---
@@ -328,11 +421,17 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sdl3/CMakeLists.txt
set(SDL_INSTALL OFF CACHE BOOL "" FORCE)
set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE)
add_subdirectory(vendor/sdl3 EXCLUDE_FROM_ALL)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/engine_smoke/CMakeLists.txt)
add_subdirectory(apps/engine_smoke)
if(NOT DEFINED _ES_DIR)
set(_ES_DIR ${CMAKE_SOURCE_DIR}/../apps/engine_smoke)
endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/runtime_test/CMakeLists.txt)
add_subdirectory(apps/runtime_test)
if(EXISTS ${_ES_DIR}/CMakeLists.txt)
add_subdirectory(${_ES_DIR} ${CMAKE_BINARY_DIR}/apps/engine_smoke)
endif()
if(NOT DEFINED _RT_DIR)
set(_RT_DIR ${CMAKE_SOURCE_DIR}/../apps/runtime_test)
endif()
if(EXISTS ${_RT_DIR}/CMakeLists.txt)
add_subdirectory(${_RT_DIR} ${CMAKE_BINARY_DIR}/apps/runtime_test)
endif()
endif()
@@ -379,3 +478,60 @@ if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
# --- dag_engine_ui (lives in apps/, issue 0096) ---
if(NOT DEFINED _DAG_UI_DIR)
set(_DAG_UI_DIR ${CMAKE_SOURCE_DIR}/../apps/dag_engine_ui)
endif()
if(EXISTS ${_DAG_UI_DIR}/CMakeLists.txt)
add_subdirectory(${_DAG_UI_DIR} ${CMAKE_BINARY_DIR}/apps/dag_engine_ui)
endif()
# --- data_factory (lives in apps/, issue 0096) ---
set(_DATA_FACTORY_DIR ${CMAKE_SOURCE_DIR}/../apps/data_factory)
if(EXISTS ${_DATA_FACTORY_DIR}/CMakeLists.txt)
add_subdirectory(${_DATA_FACTORY_DIR} ${CMAKE_BINARY_DIR}/apps/data_factory)
endif()
# --- app_hub_launcher (lives in apps/, issue 0096) ---
set(_APP_HUB_LAUNCHER_DIR ${CMAKE_SOURCE_DIR}/../apps/app_hub_launcher)
if(EXISTS ${_APP_HUB_LAUNCHER_DIR}/CMakeLists.txt)
add_subdirectory(${_APP_HUB_LAUNCHER_DIR} ${CMAKE_BINARY_DIR}/apps/app_hub_launcher)
endif()
# --- services_monitor (lives in apps/, issue 0096) ---
set(_SERVICES_MONITOR_DIR ${CMAKE_SOURCE_DIR}/../apps/services_monitor)
if(EXISTS ${_SERVICES_MONITOR_DIR}/CMakeLists.txt)
add_subdirectory(${_SERVICES_MONITOR_DIR} ${CMAKE_BINARY_DIR}/apps/services_monitor)
endif()
# --- app_gestion (lives in apps/, issue 0096) ---
set(_APP_GESTION_DIR ${CMAKE_SOURCE_DIR}/../apps/app_gestion)
if(EXISTS ${_APP_GESTION_DIR}/CMakeLists.txt)
add_subdirectory(${_APP_GESTION_DIR} ${CMAKE_BINARY_DIR}/apps/app_gestion)
endif()
# --- skill_tree (lives in apps/, issue 0096) ---
set(_SKILL_TREE_DIR ${CMAKE_SOURCE_DIR}/../apps/skill_tree)
if(EXISTS ${_SKILL_TREE_DIR}/CMakeLists.txt)
add_subdirectory(${_SKILL_TREE_DIR} ${CMAKE_BINARY_DIR}/apps/skill_tree)
endif()
# --- tables_qa (lives in apps/, issue 0096) ---
set(_TABLES_QA_DIR ${CMAKE_SOURCE_DIR}/../apps/tables_qa)
if(EXISTS ${_TABLES_QA_DIR}/CMakeLists.txt)
add_subdirectory(${_TABLES_QA_DIR} ${CMAKE_BINARY_DIR}/apps/tables_qa)
endif()
# --- process_explorer (lives in apps/, issue 0096) ---
set(_PROCESS_EXPLORER_DIR ${CMAKE_SOURCE_DIR}/../apps/process_explorer)
if(EXISTS ${_PROCESS_EXPLORER_DIR}/CMakeLists.txt)
add_subdirectory(${_PROCESS_EXPLORER_DIR} ${CMAKE_BINARY_DIR}/apps/process_explorer)
endif()
# --- agents_dashboard (lives in projects/element_agents/apps/) ---
set(_AGENTS_DASHBOARD_DIR ${CMAKE_SOURCE_DIR}/../projects/element_agents/apps/agents_dashboard)
if(EXISTS ${_AGENTS_DASHBOARD_DIR}/CMakeLists.txt)
add_subdirectory(${_AGENTS_DASHBOARD_DIR} ${CMAKE_BINARY_DIR}/apps/agents_dashboard)
endif()
Submodule cpp/apps/altsnap_jitter_test deleted from 6e52b658a3
Submodule cpp/apps/chart_demo added at 026f514bb7
-22
View File
@@ -1,22 +0,0 @@
add_imgui_app(chart_demo
main.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
# fps_overlay vive en fn_framework
)
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
if(FN_BUILD_TESTS)
add_imgui_app(chart_demo_tests
main.cpp
tests/chart_demo_tests.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
)
# Excludes int main() from main.cpp so the test target provides its own.
target_compile_definitions(chart_demo_tests PRIVATE FN_TEST_BUILD)
endif()
-60
View File
@@ -1,60 +0,0 @@
---
name: chart_demo
lang: cpp
domain: viz
description: "Demo ImGui de primitivos viz del registry: line_plot, scatter_plot, bar_chart, heatmap. Cada chart en su propia tab del TabBar. Usado como showcase y como build gate de las funciones viz/."
tags: [imgui, demo, charts, viz, showcase]
uses_functions:
- line_plot_cpp_viz
- scatter_plot_cpp_viz
- bar_chart_cpp_viz
- heatmap_cpp_viz
# logger, app_menubar viven en fn_framework — no se listan aqui
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/chart_demo"
repo_url: ""
---
## Que hace
App de una sola ventana con cuatro tabs (Line / Scatter / Bar / Heatmap) que
renderiza datos sinteticos para mostrar el aspecto y la API de los primitivos
viz del registry. Sirve como:
- **Showcase visual** de las funciones viz existentes — al añadir una nueva
primitiva, anadir su tab aqui es la forma natural de probar el binding.
- **Build gate**: si una de las funciones rompe API, esta app deja de
compilar y lo cazamos sin tener que tocar `registry_dashboard` o
`graph_explorer`.
## Estructura
`main.cpp` (~93 lineas):
- `init_data()` — genera arrays sinteticos una vez (estado modulo).
- `render()` — DockSpaceOverViewport + TabBar con 4 tabs, cada una invoca
un primitivo del registry.
- `main()``fn::run_app(...)` con AppConfig estandar (titulo, tamaño,
about, log).
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target chart_demo
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target chart_demo
```
## Decisiones
- `viewports = true` (default de `fn::run_app`): las ventanas se pueden
arrastrar fuera del main window.
- `init_gl_loader = false`: solo usa ImGui/ImPlot, sin gl* directo.
- Sin persistencia propia (no abre BD).
- `log: file_path = "chart_demo.log"` con nivel Debug — el `init_data`
emite info+debug para verificar que el logger funciona.
-89
View File
@@ -1,89 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "implot.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/bar_chart.h"
#include "viz/heatmap.h"
#include "core/app_menubar.h"
#include "core/logger.h"
#include <cmath>
#include <vector>
// Generate sample data
static constexpr int N = 500;
static float xs[N], ys_sin[N], ys_cos[N];
static float scatter_x[200], scatter_y[200];
static const char* bar_labels[] = {"Go", "Python", "Bash", "TypeScript", "C++"};
static float bar_values[] = {201.0f, 202.0f, 38.0f, 80.0f, 5.0f};
static float heat_data[10 * 10];
static bool data_initialized = false;
static void init_data() {
if (data_initialized) return;
fn_log::log_info("init_data: generando %d puntos sin/cos, 200 scatter, 10x10 heatmap", N);
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.02f;
ys_sin[i] = sinf(xs[i]);
ys_cos[i] = cosf(xs[i]);
}
for (int i = 0; i < 200; i++) {
scatter_x[i] = static_cast<float>(rand()) / RAND_MAX * 10.0f;
scatter_y[i] = scatter_x[i] * 0.5f + (static_cast<float>(rand()) / RAND_MAX - 0.5f) * 3.0f;
}
for (int i = 0; i < 100; i++) {
int r = i / 10, c = i % 10;
heat_data[i] = sinf(r * 0.5f) * cosf(c * 0.5f);
}
data_initialized = true;
fn_log::log_debug("init_data: ok");
}
void render() {
init_data();
if (ImGui::Begin("fn_registry — Chart Demo")) {
if (ImGui::BeginTabBar("##charts")) {
if (ImGui::BeginTabItem("Line Plot")) {
ImGui::Text("sin(x) — %d points", N);
line_plot("Sine Wave", xs, ys_sin, N);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scatter Plot")) {
ImGui::Text("y = 0.5x + noise — 200 points");
scatter_plot("Scatter Data", scatter_x, scatter_y, 200);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Bar Chart")) {
ImGui::Text("Functions per language in fn_registry");
bar_chart("Registry Languages", bar_labels, bar_values, 5);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Heatmap")) {
ImGui::Text("sin(r) * cos(c) — 10x10 matrix");
heatmap("Correlation Matrix", heat_data, 10, 10, -1.0f, 1.0f);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "fn_registry — Chart Demo",
.width = 1400,
.height = 900,
.about = {.name = "chart demo",
.version = "0.2.0",
.description = "Demo de primitivos viz: line, scatter, bar, heatmap. AppConfig estandar + multi-viewport."},
.log = {.file_path = "chart_demo.log",
.level = static_cast<int>(fn_log::Level::Debug)}
}, render);
}
#endif
@@ -1,41 +0,0 @@
// E2E tests for chart_demo — Dear ImGui Test Engine.
// Built only when -DFN_BUILD_TESTS=ON. The same main.cpp from chart_demo is
// compiled here with FN_TEST_BUILD defined so its int main() is excluded and
// only render() is reused.
#include "app_base.h"
#include "imgui.h"
#include "imgui_te_engine.h"
#include "imgui_te_context.h"
void render(); // defined in chart_demo/main.cpp
static void register_tests(ImGuiTestEngine* e) {
ImGuiTest* t = nullptr;
// Smoke test: the main window appears and is non-empty.
t = IM_REGISTER_TEST(e, "chart_demo", "smoke_window_visible");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo"); // em-dash
IM_CHECK(ctx->WindowInfo("").ID != 0);
};
// Cycle through all four tabs. Test engine fails the test if any tab item
// is not found or cannot be activated — that is our implicit assertion.
t = IM_REGISTER_TEST(e, "chart_demo", "tabs_cycle_all");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo");
ctx->ItemClick("##charts/Line Plot");
ctx->ItemClick("##charts/Scatter Plot");
ctx->ItemClick("##charts/Bar Chart");
ctx->ItemClick("##charts/Heatmap");
};
}
int main() {
fn::AppConfig cfg{};
cfg.title = "chart_demo_tests";
cfg.width = 1280;
cfg.height = 800;
return fn::run_app_test(cfg, render, register_tests);
}
Submodule cpp/apps/engine_smoke deleted from bed33856e7
-110
View File
@@ -1,110 +0,0 @@
add_imgui_app(primitives_gallery
main.cpp
capture.cpp
demo.cpp
demos_core.cpp
demos_viz.cpp
demos_graph.cpp
demos_graph_styles.cpp
demos_gfx.cpp
demos_3d.cpp
demos_text_editor.cpp
demos_gl_texture.cpp
demos_extras.cpp
demos_mesh.cpp
# animation primitives (issue 0031)
demos_animation.cpp
${CMAKE_SOURCE_DIR}/functions/core/tween_curves.cpp
${CMAKE_SOURCE_DIR}/functions/core/bezier_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
demos_sql.cpp
demos_scientific.cpp
# text_editor + file_watcher (issue 0025) + file_poll_diff pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
# sql_workbench (issue 0032) + sql_parse pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.cpp
# Core primitives demoed (tokens vive en fn_framework)
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
${CMAKE_SOURCE_DIR}/functions/core/icon_button.cpp
${CMAKE_SOURCE_DIR}/functions/core/toolbar.cpp
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
${CMAKE_SOURCE_DIR}/functions/core/select.cpp
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_state_machine.cpp
# Viz primitives demoed
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/pie_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/histogram.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
${CMAKE_SOURCE_DIR}/functions/viz/candlestick.cpp
${CMAKE_SOURCE_DIR}/functions/viz/gauge.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
# 3D viz primitives (issue 0028, ImPlot3D)
${CMAKE_SOURCE_DIR}/functions/viz/surface_plot_3d.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_3d.cpp
# Scientific viz (issue 0034)
${CMAKE_SOURCE_DIR}/functions/viz/treemap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sankey.cpp
${CMAKE_SOURCE_DIR}/functions/viz/chord.cpp
${CMAKE_SOURCE_DIR}/functions/viz/contour.cpp
${CMAKE_SOURCE_DIR}/functions/viz/voronoi.cpp
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_icons.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_layouts.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport_selection.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels_select.cpp
${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp
# GL loader (Linux no-op, Windows wglGetProcAddress)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
# Shader stack (shader_canvas demo)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
# gl_texture_load (issue 0026) + stb_image
${CMAKE_SOURCE_DIR}/functions/gfx/gl_texture_load.cpp
${CMAKE_SOURCE_DIR}/vendor/stb/stb_image_impl.cpp
# mesh_viewer stack (issue 0029)
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_obj_load.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp
${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp
)
target_include_directories(primitives_gallery PRIVATE
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
${CMAKE_SOURCE_DIR}/vendor/stb
)
# SQLite (sql_workbench) — alias provisto por cpp/CMakeLists.txt:
# system on Linux, vendored amalgamation on Windows cross-compile.
target_link_libraries(primitives_gallery PRIVATE SQLite::SQLite3)
if(WIN32)
target_link_libraries(primitives_gallery PRIVATE opengl32)
endif()
if(WIN32)
set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
-159
View File
@@ -1,159 +0,0 @@
# primitives_gallery
Catalogo visual interactivo de los primitivos UI del registry (`cpp/functions/core` y `cpp/functions/viz`). Un solo ejecutable con sidebar izquierdo + panel derecho que renderiza la demo del primitivo seleccionado con todas sus variantes y un snippet de codigo.
## Rol
| Funcion | Como lo cumple |
|---|---|
| Smoke test visual | Abrir la gallery tras un cambio en tokens / componentes; si algo se ve raro, lo cazas en segundos. |
| Documentacion viva | Cada demo muestra el componente trabajando + el snippet exacto. Mas rapido que leer los `.md`. |
| Build gate | Esta en el CMake principal (`cpp/CMakeLists.txt`). Si un primitivo rompe API, la gallery no compila => CI rojo. |
| Sandbox de prototipos | Datos sinteticos, sin backend; ideal para iterar un primitivo nuevo sin tocar el dashboard. |
## Build & run
```bash
# Linux
cmake --build cpp/build/linux --target primitives_gallery -j$(nproc)
./cpp/build/linux/apps/primitives_gallery/primitives_gallery
# Windows (cross-compile)
cmake --build cpp/build/windows --target primitives_gallery -j$(nproc)
# binario: cpp/build/windows/apps/primitives_gallery/primitives_gallery.exe
```
No se conecta a `sqlite_api` ni a ningun backend. Datos sinteticos generados in-memory.
## Demos disponibles
### Core
| Demo | Primitivo | Que muestra |
|---|---|---|
| button | `button_cpp_core` | 4 variantes x 3 sizes |
| icon_button | `icon_button_cpp_core` | Glyphs comunes con tooltip |
| toolbar | `toolbar_cpp_core` | Dos grupos con separador vertical |
| modal_dialog | `modal_dialog_cpp_core` | Boton que abre modal con form |
| text_input | `text_input_cpp_core` | 3 inputs con placeholder |
| select | `select_cpp_core` | Dropdown con y sin `(none)` |
| toast + inbox | `toast_cpp_core` (v1.1) | 4 botones que disparan toasts + campana con badge |
| tree_view | `tree_view_cpp_core` | Arbol fake de proyectos -> apps |
| badge | `badge_cpp_core` | 6 variantes semanticas |
| empty_state | `empty_state_cpp_core` | Lista vacia con icono + cta |
| page_header | `page_header_cpp_core` | Header con toolbar a la derecha |
| dashboard_panel | `dashboard_panel_cpp_core` | Panel con titulo y borde |
| kpi_card | `kpi_card_cpp_viz` (v1.2) | Grid 1x4 con sparklines y delta |
### Viz
| Demo | Primitivo | Que muestra |
|---|---|---|
| bar_chart | `bar_chart_cpp_viz` (v1.2) | Labels que caben + labels rotados 45 |
| pie_chart | `pie_chart_cpp_viz` (v1.1) | Pie + donut con tooltip por slice |
| line_plot | `line_plot_cpp_viz` (v1.1) | Serie sintetica `sin(t) + ruido` |
| scatter_plot | `scatter_plot_cpp_viz` (v1.1) | 120 puntos con correlacion |
| histogram | `histogram_cpp_viz` (v1.1) | 300 muestras gaussianas |
| sparkline | `sparkline_cpp_viz` | Trending up / down / flat |
| graph_viewport | `graph_viewport_cpp_viz` | **Ver seccion abajo** |
## Demo `graph_viewport` (en detalle)
Pipeline completo de visualizacion de grafos con instanced GPU rendering:
- `graph_renderer_cpp_viz` (1 draw call para todos los nodos via `glDrawArraysInstanced`)
- `graph_force_layout_cpp_viz` (Barnes-Hut, paso de simulacion por frame)
- `graph_spatial_hash_cpp_core` (hit-testing O(1) bajo el cursor)
- `graph_viewport_cpp_viz` (widget que orquesta los anteriores con pan/zoom/select)
### Controles
| Control | Rango | Efecto |
|---|---|---|
| `Nodes` | 100 20 000 | Numero de nodos a generar |
| `Clusters` | 2 16 | Numero de comunidades (cada una con su color) |
| `Repulsion` | 100 20 000 | Fuerza repulsiva entre todos los nodos. Mas alto => grafo mas extendido y energia mayor. |
| `Attraction` | 0.001 0.5 | Constante del muelle de las aristas. Mas alto => clusters mas compactos. |
| `Gravity` | 0.0 0.05 | Tiron hacia (0,0). Util para evitar drift cuando subes mucho la repulsion. |
| `Regenerate` | boton | Regenera el grafo con los valores actuales de Nodes/Clusters. |
| `Pause / Resume layout` | boton | Para o reanuda la simulacion force-directed. |
| `Fit view` | boton | Encuadra la camara al bounding box del grafo con 10% de padding. |
Los tres sliders de fuerzas se leen cada frame y se inyectan en `ForceLayoutConfig`, asi que cambiar un valor durante el layout en marcha re-calibra el sistema al instante.
### Stats line (sin vibracion)
Una sola linea fija — sin secciones condicionales que cambien la altura del panel:
```
nodes=N edges=E energy=X fps=F | hover=#id cN sel=#id
```
`hover` y `sel` muestran `-` cuando no hay nada seleccionado para mantener el ancho/alto estable; antes una fila condicional desplazaba el viewport en cada hover.
### Interaccion con el viewport
| Gesto | Accion |
|---|---|
| Drag con boton izquierdo en zona vacia | Pan de camara |
| Wheel | Zoom (limites 0.01x 50x) |
| Drag sobre nodo | Mueve el nodo (lo `pin`ea durante el drag) |
| Click sobre nodo | Selecciona (`s_state.selected_node`) |
| Hover sobre nodo | Resaltado + `s_state.hovered_node` poblado |
### Datos sinteticos
`generate_synthetic_graph(N, K)` reparte N nodos en K clusters dispuestos en circulo, con ~3 aristas intra-cluster por nodo y un 5% adicional de aristas inter-cluster. Paleta de 8 colores ABGR. Posiciones iniciales con dispersion gaussiana de 80 px alrededor del centroide del cluster — el force layout las reordena en pocos frames.
### Performance esperada
| Nodes | FPS objetivo (RTX 30xx, viewport 800x460) | Notas |
|---|---|---|
| 1 000 | 60 (vsync) | Caso comun; layout converge < 1 s |
| 5 000 | 60 | Pipeline al limite del CPU para Barnes-Hut |
| 20 000 | 30 50 | El cuello pasa a ser el layout (CPU); GPU render sigue holgado |
Si necesitas mas, fija los nodos (`pinned = true` o `Pause layout`) y veras 60 fps estables — el bottleneck es la simulacion, no el render.
## Anadir un demo nuevo
1. Anadir el prototipo en `demos.h` dentro de `namespace gallery`:
```cpp
void demo_my_thing();
```
2. Implementar el cuerpo en `demos_core.cpp` o `demos_viz.cpp` (o un fichero nuevo si la demo es grande, p.ej. `demos_graph.cpp`).
3. Registrar la entrada en el array `k_demos[]` de `main.cpp`:
```cpp
{"my_thing", "my_thing", "Core" /* o "Viz" */, &gallery::demo_my_thing},
```
4. Si la demo necesita `.cpp` adicionales del registry, anadirlos a `CMakeLists.txt` de la gallery.
5. Recompilar.
## Estructura
```
cpp/apps/primitives_gallery/
CMakeLists.txt # target primitives_gallery
README.md # este fichero
main.cpp # sidebar + router
demo.{h,cpp} # helpers (demo_header, section, code_block, ...)
demos.h # prototipos void demo_xxx()
demos_core.cpp # demos del dominio core
demos_viz.cpp # demos del dominio viz (charts simples)
demos_graph.cpp # demo de graph_viewport (mas pesada, fichero aparte)
```
## Convenciones para los demos
- **Sin estado real**: usar arrays sinteticos (`float fake[] = {...}`) o generadores deterministas con seed fijo. Datos reproducibles.
- **Sin red**: nunca llamar a `sqlite_api`, HTTP, filesystem. La gallery debe arrancar offline en cualquier maquina.
- **Snippets honestos**: el `code_block(...)` debe mostrar el codigo que produce esa demo, no pseudocodigo.
- **Variantes en grids**: si un primitivo tiene N variantes x M tamanos, mostrarlos todos en un `BeginTable` para comparacion lado-a-lado.
- **Estado static**: si la demo es interactiva (sliders, modal, etc.), guardar el estado en `static` locales — la gallery no destruye demos al cambiar de seccion, asi que el estado persiste hasta cerrar la app.
## Iconos en los demos
A partir de la sesion 2026-04-25 los demos usan los macros `TI_*` de `cpp/functions/core/icons_tabler.h` (Tabler v3.41.1, 5093 glyphs). La fuente la carga automaticamente `fn::run_app` via `icon_font_cpp_core`, y `add_imgui_app` copia `tabler-icons.ttf` junto al ejecutable post-build (no hay paso manual).
`demo_icon_button` y `demo_toolbar` (en `demos_core.cpp`) son la referencia visual: muestran el patron `button(TI_PLUS " New", V::Primary)` y la fila de iconos sueltos. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla.
Si añades un demo nuevo y necesitas glyphs, **no metas `\x..` UTF-8 inline** — busca el icono en `icons_tabler.h` (o en https://tabler.io/icons) y usa el `TI_*` correspondiente.
-37
View File
@@ -1,37 +0,0 @@
---
name: primitives_gallery
lang: cpp
domain: gfx
description: "Visual catalog de primitivas C++ UI del fn_registry. Demos por categoria (charts, controls, layout, gl_info). Soporta modo --capture para regresion visual."
tags: [imgui, gallery, gfx, demo, capture]
uses_functions: []
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/primitives_gallery"
repo_url: ""
---
# primitives_gallery
Catalogo visual de las primitivas y componentes ImGui del registry. Cada demo se carga al hacer click en su entrada del sidebar.
## Build & run
```bash
cd cpp && cmake --build build --target primitives_gallery -j
./build/primitives_gallery
```
## Modo capture (regresion visual)
```bash
./build/primitives_gallery --capture <out_dir>
```
Renderiza cada demo offscreen y guarda PNGs en `<out_dir>/`. Permite gate visual via golden images.
## Notas
- `auto_dockspace = false` — usa `fullscreen_window` que ocupa todo el viewport.
- `init_gl_loader = true` — necesario para demos de OpenGL 4.3 core (compute, SSBOs).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 B

-173
View File
@@ -1,173 +0,0 @@
// Implementacion de gallery::run_capture — render offscreen + glReadPixels +
// PNG via stb_image_write. Ver capture.h.
#include "capture.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "implot.h"
#include "implot3d.h"
#include "core/tokens.h"
#include "core/icon_font.h"
#include "core/app_settings.h"
#include "gfx/gl_loader.h"
#include <GLFW/glfw3.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <cstdio>
#include <vector>
namespace gallery {
static void glfw_capture_error(int error, const char* description) {
std::fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}
// Flip vertical in-place: OpenGL origin = bottom-left, PNG = top-left.
static void flip_vertical_rgba(unsigned char* px, int w, int h) {
const int stride = w * 4;
std::vector<unsigned char> row(stride);
for (int y = 0; y < h / 2; ++y) {
unsigned char* a = px + y * stride;
unsigned char* b = px + (h - 1 - y) * stride;
std::copy(a, a + stride, row.begin());
std::copy(b, b + stride, a);
std::copy(row.begin(), row.end(), b);
}
}
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items) {
glfwSetErrorCallback(&glfw_capture_error);
if (!glfwInit()) {
std::fprintf(stderr, "capture: glfwInit failed\n");
return false;
}
// Capture mode usa GL 3.3 deliberadamente: WSL Mesa no entrega contexto
// 4.3 offscreen (GLXBadFBConfig). Las pruebas visuales no necesitan
// compute/SSBO — ImGui+ImPlot funciona en 3.3 core. La build interactiva
// (app_base.cpp) si pide 4.3.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
GLFWwindow* window = glfwCreateWindow(
cfg.capture_w, cfg.capture_h, "capture", nullptr, nullptr);
if (!window) {
std::fprintf(stderr, "capture: glfwCreateWindow failed (no GL?)\n");
glfwTerminate();
return false;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(0);
if (!fn::gfx::gl_loader_init()) {
std::fprintf(stderr, "capture: gl_loader_init failed\n");
glfwDestroyWindow(window);
glfwTerminate();
return false;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImPlot3D::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr; // no .ini side effects in capture mode.
io.DisplaySize = ImVec2((float)cfg.capture_w, (float)cfg.capture_h);
fn_ui::settings_load();
fn_ui::load_fonts_from_settings();
{
ImGuiStyle& style = ImGui::GetStyle();
style.FontSizeBase = fn_ui::settings().font_size_px;
style._NextFrameFontSizeBase = style.FontSizeBase;
}
fn_tokens::apply_dark_theme();
ImGui_ImplGlfw_InitForOpenGL(window, false);
ImGui_ImplOpenGL3_Init("#version 330");
bool ok_all = true;
std::vector<unsigned char> pixels((size_t)cfg.capture_w * cfg.capture_h * 4u);
for (const auto& item : items) {
// Warmup: rinde varios frames para que ImGui/ImPlot estabilicen layout
// (el primer frame frecuentemente carece de mediciones de tamaño).
for (int frame = 0; frame < cfg.warmup_frames + 1; ++frame) {
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Ventana fullscreen sobre el viewport con la demo activa,
// sin sidebar (queremos el render del primitivo lo mas limpio
// posible para el diff visual).
const ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(vp->WorkPos);
ImGui::SetNextWindowSize(vp->WorkSize);
ImGui::Begin("##capture_root",
nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoSavedSettings);
if (item.fn) item.fn();
ImGui::End();
ImGui::Render();
int dw, dh;
glfwGetFramebufferSize(window, &dw, &dh);
glViewport(0, 0, dw, dh);
glClearColor(fn_tokens::colors::bg.x,
fn_tokens::colors::bg.y,
fn_tokens::colors::bg.z, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
// Read framebuffer (GL_RGBA / GL_UNSIGNED_BYTE).
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, cfg.capture_w, cfg.capture_h,
GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
flip_vertical_rgba(pixels.data(), cfg.capture_w, cfg.capture_h);
char path[1024];
std::snprintf(path, sizeof(path), "%s/%s.png",
cfg.output_dir.c_str(), item.id.c_str());
const int rc = stbi_write_png(
path, cfg.capture_w, cfg.capture_h, 4,
pixels.data(), cfg.capture_w * 4);
if (rc == 0) {
std::fprintf(stderr, "capture: stbi_write_png failed for %s\n", path);
ok_all = false;
} else {
std::fprintf(stdout, "captured: %s\n", path);
}
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot3D::DestroyContext();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return ok_all;
}
} // namespace gallery
-34
View File
@@ -1,34 +0,0 @@
#pragma once
// Capture mode: renderiza cada demo de la gallery en una ventana GLFW
// invisible y guarda un PNG en `output_dir/<demo_id>.png` via stb_image_write.
//
// Diseñado para CI / golden-image diffing: ver `cpp/scripts/update_goldens.sh`
// y `cpp/tests/test_visual.cpp`.
//
// Importante:
// - Requiere un contexto OpenGL real. En entornos sin GPU (containers minimos)
// funciona con `LIBGL_ALWAYS_SOFTWARE=1` (Mesa/llvmpipe) o swiftshader.
// - Si el entorno (WSL sin GL) no puede crear un contexto GL valido, el
// binario sale con codigo != 0 sin generar PNGs.
#include <string>
#include <vector>
namespace gallery {
struct CaptureItem {
std::string id;
void (*fn)();
};
struct CaptureConfig {
std::string output_dir;
int warmup_frames = 3;
int capture_w = 800;
int capture_h = 600;
};
// Devuelve true si todo el set se capturo OK.
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items);
} // namespace gallery
-76
View File
@@ -1,76 +0,0 @@
#include "demo.h"
#include "core/tokens.h"
#include <cstdio>
namespace gallery {
void demo_header(const char* name, const char* version, const char* description) {
using namespace fn_tokens;
ImGui::SetWindowFontScale(1.4f);
ImGui::TextUnformatted(name);
ImGui::SetWindowFontScale(1.0f);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text(" %s", version);
ImGui::PopStyleColor();
if (description && *description) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("%s", description);
ImGui::PopStyleColor();
}
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::sm));
}
void section(const char* title) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted(title);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::xs));
}
void variant_label(const char* text) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
}
void code_block(const char* code) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("// example");
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
// Altura: aprox lineas * line-height
int lines = 1;
for (const char* p = code; *p; ++p) if (*p == '\n') ++lines;
float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md;
char id[32];
std::snprintf(id, sizeof(id), "##code_%p", (const void*)code);
ImGui::BeginChild(id, ImVec2(0, h),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::TextUnformatted(code);
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(2);
}
} // namespace gallery
-22
View File
@@ -1,22 +0,0 @@
#pragma once
// Helpers compartidos por todas las demos de la gallery.
// No son primitivos del registry — son utilidades locales de este app.
#include "imgui.h"
#include <string>
namespace gallery {
// Titulo + version + descripcion en la parte superior del panel derecho.
void demo_header(const char* name, const char* version, const char* description);
// Seccion secundaria dentro de una demo (agrupar variantes).
void section(const char* title);
// Bloque de codigo monoespaciado con bg surface y label "// example".
void code_block(const char* code);
// Etiqueta sutil encima de un grupo de widgets.
void variant_label(const char* text);
} // namespace gallery
-56
View File
@@ -1,56 +0,0 @@
#pragma once
// Cada demo_xxx() renderiza una seccion completa para un primitivo.
// Se llaman desde main.cpp en funcion del item seleccionado en el sidebar.
namespace gallery {
// --- Core ---
void demo_button();
void demo_icon_button();
void demo_toolbar();
void demo_modal();
void demo_text_input();
void demo_select();
void demo_toast();
void demo_tree_view();
void demo_kpi_card();
void demo_badge();
void demo_empty_state();
void demo_page_header();
void demo_dashboard_panel();
void demo_text_editor(); // wave 1, issue 0025
void demo_file_watcher(); // wave 1, issue 0025
void demo_process_runner();
void demo_tween(); // issue 0031
void demo_bezier_editor(); // issue 0031
void demo_timeline(); // issue 0031
void demo_sql_workbench(); // issue 0032
// --- Viz ---
void demo_bar_chart();
void demo_pie_chart();
void demo_line_plot();
void demo_scatter_plot();
void demo_histogram();
void demo_sparkline();
void demo_graph();
void demo_graph_styles(); // issue 0049f
void demo_candlestick();
void demo_gauge();
void demo_heatmap();
void demo_table_view();
void demo_surface_plot_3d(); // issue 0028, ImPlot3D
void demo_scatter_3d(); // issue 0028, ImPlot3D
void demo_mesh_viewer(); // issue 0029
void demo_treemap(); // issue 0034
void demo_sankey(); // issue 0034
void demo_chord(); // issue 0034
void demo_contour(); // issue 0034
void demo_voronoi(); // issue 0034
// --- Gfx ---
void demo_shader_canvas();
void demo_gl_texture(); // wave 1, issue 0026
void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps
} // namespace gallery
-100
View File
@@ -1,100 +0,0 @@
// demos_3d — demos para los primitivos viz/* basados en ImPlot3D.
// Issue 0028: surface_plot_3d real + scatter_3d.
#include "demos.h"
#include "demo.h"
#include "viz/surface_plot_3d.h"
#include "viz/scatter_3d.h"
#include <imgui.h>
#include <cmath>
#include <random>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// surface_plot_3d
// ---------------------------------------------------------------------------
void demo_surface_plot_3d() {
demo_header("surface_plot_3d", "v2.0.0",
"Superficie 3D ImPlot3D (z = A * sin(fx*x) * cos(fy*y)) con sliders para "
"ajustar las frecuencias en tiempo real. Drag para orbitar, wheel para zoom.");
section("Malla 64x64 — sin(fx*x) * cos(fy*y)");
static float fx = 0.20f;
static float fy = 0.20f;
static float amp = 1.0f;
ImGui::SliderFloat("fx", &fx, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("fy", &fy, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("amplitud", &amp, 0.1f, 3.0f, "%.2f");
constexpr int N = 64;
static std::vector<float> z(N * N);
for (int j = 0; j < N; ++j) {
for (int i = 0; i < N; ++i) {
z[j * N + i] = amp * std::sin(fx * float(i)) * std::cos(fy * float(j));
}
}
fn::SurfacePlot3DConfig cfg{};
cfg.z = z.data();
cfg.nx = N; cfg.ny = N;
cfg.x_min = 0.f; cfg.x_max = float(N);
cfg.y_min = 0.f; cfg.y_max = float(N);
cfg.size = ImVec2(-1.f, 420.f);
fn::surface_plot_3d("##gallery_surface", cfg);
}
// ---------------------------------------------------------------------------
// scatter_3d
// ---------------------------------------------------------------------------
void demo_scatter_3d() {
demo_header("scatter_3d", "v1.0.0",
"Scatter 3D ImPlot3D con color por punto. 3 clusters gaussianos sinteticos "
"(N=500) para simular una visualizacion tipica de PCA / clustering.");
section("3 clusters gaussianos (500 puntos)");
constexpr int N = 500;
static std::vector<float> xs(N), ys(N), zs(N);
static std::vector<ImU32> colors(N);
static bool initialized = false;
if (!initialized) {
std::mt19937 rng(42);
std::normal_distribution<float> g(0.f, 0.4f);
const ImU32 palette[3] = {
IM_COL32(255, 99, 71, 255), // tomate
IM_COL32( 65, 170, 255, 255), // azul
IM_COL32(120, 220, 120, 255), // verde
};
const float cx[3] = {-1.5f, 1.5f, 0.f};
const float cy[3] = { 0.f, 0.f, 2.0f};
const float cz[3] = { 0.f, 1.0f,-1.0f};
for (int i = 0; i < N; ++i) {
int c = i % 3;
xs[i] = cx[c] + g(rng);
ys[i] = cy[c] + g(rng);
zs[i] = cz[c] + g(rng);
colors[i] = palette[c];
}
initialized = true;
}
fn::Scatter3DConfig cfg{};
cfg.xs = xs.data();
cfg.ys = ys.data();
cfg.zs = zs.data();
cfg.colors = colors.data();
cfg.n = N;
cfg.size = ImVec2(-1.f, 420.f);
fn::scatter_3d("##gallery_clusters", cfg);
}
} // namespace gallery
@@ -1,249 +0,0 @@
// Demos para los primitivos de animacion (issue 0031):
// - tween_curves
// - bezier_editor
// - timeline
#include "demos.h"
#include "demo.h"
#include "core/tween_curves.h"
#include "core/bezier_editor.h"
#include "core/timeline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
#include <cmath>
namespace gallery {
// ---------------------------------------------------------------------------
// demo_tween — dropdown + plot animado
// ---------------------------------------------------------------------------
void demo_tween() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("tween_curves", "v1.0.0",
"Funciones de easing (Penner): linear, quad, cubic, expo, elastic, "
"bounce con variantes in/out/inOut. Header-mostly: el compilador "
"inlinea cada curva en el sitio de llamada.");
section("Selector + plot");
static int ease_idx = (int)Ease::OutCubic;
static float anim_t = 0.0f;
anim_t += ImGui::GetIO().DeltaTime * 0.5f;
if (anim_t > 1.5f) anim_t = -0.25f; // hold un poco antes de reiniciar
// Build labels
const char* labels[fn::tween::ease_count];
for (int i = 0; i < fn::tween::ease_count; i++) {
labels[i] = fn::tween::name((Ease)i);
}
ImGui::SetNextItemWidth(220.0f);
ImGui::Combo("##tween_ease", &ease_idx, labels, fn::tween::ease_count);
Ease ease = (Ease)ease_idx;
float t_clamped = anim_t;
if (t_clamped < 0.0f) t_clamped = 0.0f;
if (t_clamped > 1.0f) t_clamped = 1.0f;
float v = fn::tween::apply(ease, t_clamped);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text(" t=%.2f f(t)=%.3f", t_clamped, v);
ImGui::PopStyleColor();
// Canvas plot
ImVec2 canvas_min = ImGui::GetCursorScreenPos();
ImVec2 canvas_size(360.0f, 220.0f);
ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x, canvas_min.y + canvas_size.y);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm);
dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm);
auto to_px = [&](float tx, float ty) {
// ty puede salir de [0,1] (elastic/bounce); damos algo de margen vertical.
return ImVec2(canvas_min.x + tx * canvas_size.x,
canvas_min.y + (1.0f - ty) * canvas_size.y);
};
// Grid 4x4
ImU32 grid = ImGui::GetColorU32(colors::border);
for (int i = 1; i < 4; i++) {
float fx = canvas_min.x + canvas_size.x * (float)i / 4.0f;
float fy = canvas_min.y + canvas_size.y * (float)i / 4.0f;
dl->AddLine(ImVec2(fx, canvas_min.y), ImVec2(fx, canvas_max.y), grid);
dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid);
}
// Diagonal linear
dl->AddLine(to_px(0.0f, 0.0f), to_px(1.0f, 1.0f),
ImGui::GetColorU32(colors::text_dim), 1.0f);
// Curva
constexpr int N = 96;
ImVec2 prev = to_px(0.0f, fn::tween::apply(ease, 0.0f));
ImU32 col = ImGui::GetColorU32(colors::primary);
for (int i = 1; i <= N; i++) {
float x = (float)i / (float)N;
float y = fn::tween::apply(ease, x);
ImVec2 cur = to_px(x, y);
dl->AddLine(prev, cur, col, 2.0f);
prev = cur;
}
// Marker animado
ImVec2 m = to_px(t_clamped, v);
dl->AddCircleFilled(m, 5.0f, ImGui::GetColorU32(colors::primary_light));
dl->AddCircle(m, 6.0f, ImGui::GetColorU32(colors::text), 0, 1.5f);
// Avanzar cursor
ImGui::Dummy(canvas_size);
code_block(
"#include \"core/tween_curves.h\"\n\n"
"float k = fn::tween::apply(fn::tween::Ease::OutCubic, t);\n"
"// o named:\n"
"float k2 = fn::tween::out_cubic(t);"
);
}
// ---------------------------------------------------------------------------
// demo_bezier_editor — editor + plot evaluado
// ---------------------------------------------------------------------------
void demo_bezier_editor() {
using namespace fn_tokens;
demo_header("bezier_editor", "v1.0.0",
"Editor visual de curva Bezier cubica (4 puntos). Para diseñar "
"easing curves custom. p1/p2 son draggable; p0/p3 fijos en (0,0)/(1,1).");
section("Editor");
static fn::BezierCurve curve; // identidad por defecto: ease lineal con handles desplazados
if (ImGui::Button("Reset##bz_reset")) {
curve = fn::BezierCurve{};
}
ImGui::SameLine();
if (ImGui::Button("Ease-out preset##bz_eo")) {
curve = {{0,0}, {0.0f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
ImGui::SameLine();
if (ImGui::Button("Ease-in-out preset##bz_eio")) {
curve = {{0,0}, {0.42f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
fn::bezier_editor("##bz_editor", curve, ImVec2(220, 220));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("p0=(%.2f,%.2f) p1=(%.2f,%.2f) p2=(%.2f,%.2f) p3=(%.2f,%.2f)",
curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y,
curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y);
ImGui::PopStyleColor();
// Plot evaluation
section("bezier_eval(curve, t)");
static float t = 0.0f;
ImGui::SetNextItemWidth(360.0f);
ImGui::SliderFloat("t##bz_t", &t, 0.0f, 1.0f, "%.3f");
float y = fn::bezier_eval(curve, t);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("y(t=%.3f) = %.3f", t, y);
ImGui::PopStyleColor();
code_block(
"#include \"core/bezier_editor.h\"\n\n"
"static fn::BezierCurve curve;\n"
"if (fn::bezier_editor(\"##my\", curve, ImVec2(220, 220))) {\n"
" // user dragged a control point\n"
"}\n"
"float k = fn::bezier_eval(curve, t);"
);
}
// ---------------------------------------------------------------------------
// demo_timeline — 2 tracks + display
// ---------------------------------------------------------------------------
void demo_timeline() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("timeline", "v1.0.0",
"Timeline tipo DAW: tracks horizontales con keyframes draggable, "
"scrub con el ruler, play/pause/loop. track_value_at(t) interpola "
"aplicando la Ease de cada keyframe destino.");
static fn::TimelineState tl;
static bool inited = false;
if (!inited) {
tl.duration = 4.0f;
tl.playing = true;
tl.tracks.push_back({"hue", {
{0.0f, 0.0f, Ease::Linear},
{2.0f, 1.0f, Ease::OutCubic},
{4.0f, 0.0f, Ease::InOutCubic},
}});
tl.tracks.push_back({"amp", {
{0.0f, 0.2f, Ease::Linear},
{3.0f, 1.0f, Ease::OutElastic},
}});
inited = true;
}
// Update
fn::timeline_update(tl, ImGui::GetIO().DeltaTime);
// Display values
section("Live values");
float hue = fn::track_value_at(tl.tracks[0], tl.current_time);
float amp = fn::track_value_at(tl.tracks[1], tl.current_time);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::Text("t = %.3fs", tl.current_time);
ImGui::PopStyleColor();
auto draw_bar = [&](const char* name, float value, float vmin, float vmax) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("%-4s", name);
ImGui::PopStyleColor();
ImGui::SameLine();
ImVec2 cmin = ImGui::GetCursorScreenPos();
ImVec2 csize = ImVec2(280.0f, 14.0f);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x, cmin.y + csize.y),
ImGui::GetColorU32(colors::surface_active), radius::sm);
float k = (value - vmin) / (vmax - vmin);
if (k < 0.0f) k = 0.0f;
if (k > 1.0f) k = 1.0f;
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x * k, cmin.y + csize.y),
ImGui::GetColorU32(colors::primary), radius::sm);
ImGui::Dummy(csize);
ImGui::SameLine();
ImGui::Text("%.3f", value);
};
draw_bar("hue", hue, 0.0f, 1.0f);
draw_bar("amp", amp, 0.0f, 1.0f);
section("Widget");
fn::timeline_widget("##gallery_tl", tl, ImVec2(-1, 220));
code_block(
"#include \"core/timeline.h\"\n\n"
"static fn::TimelineState tl;\n"
"tl.tracks.push_back({\"hue\", {{0,0}, {2,1, fn::tween::Ease::OutCubic}, {4,0}}});\n"
"tl.duration = 4.0f; tl.playing = true;\n\n"
"fn::timeline_update(tl, ImGui::GetIO().DeltaTime);\n"
"float h = fn::track_value_at(tl.tracks[0], tl.current_time);\n"
"fn::timeline_widget(\"##tl\", tl);"
);
}
} // namespace gallery
-447
View File
@@ -1,447 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "core/button.h"
#include "core/icon_button.h"
#include "core/toolbar.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/select.h"
#include "core/toast.h"
#include "core/tree_view.h"
#include "core/badge.h"
#include "core/empty_state.h"
#include "core/page_header.h"
#include "core/dashboard_panel.h"
#include "core/tokens.h"
#include "core/icons_tabler.h"
#include "viz/kpi_card.h"
#include <imgui.h>
#include <cstdio>
using namespace fn_ui;
using V = ButtonVariant;
using S = ButtonSize;
namespace gallery {
// ---------------------------------------------------------------------------
// button
// ---------------------------------------------------------------------------
void demo_button() {
demo_header("button", "v1.0.0",
"Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, "
"radius y padding — estilo consistente en toda la app.");
section("Variants x Sizes");
const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger};
const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"};
const S sizes[] = {S::Sm, S::Md, S::Lg};
const char* size_names[] = {"sm", "md", "lg"};
if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("size");
for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]);
ImGui::TableHeadersRow();
for (int s = 0; s < 3; s++) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
variant_label(size_names[s]);
for (int v = 0; v < 4; v++) {
ImGui::TableSetColumnIndex(v + 1);
char id[32];
std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v);
button(id, variants[v], sizes[s]);
}
}
ImGui::EndTable();
}
code_block(
"#include \"core/button.h\"\n"
"using fn_ui::button;\n"
"using V = fn_ui::ButtonVariant;\n\n"
"if (button(\"Save\", V::Primary)) save();\n"
"if (button(\"Cancel\", V::Subtle)) close();\n"
"if (button(\"Delete\", V::Danger)) confirm();"
);
}
// ---------------------------------------------------------------------------
// icon_button
// ---------------------------------------------------------------------------
void demo_icon_button() {
demo_header("icon_button", "v1.0.0",
"Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. "
"Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente "
"por fn::run_app via icon_font.cpp).");
section("Tabler icon set");
struct { const char* id; const char* glyph; const char* tip; } ic[] = {
{"##rl", TI_REFRESH, "Reload"},
{"##ad", TI_PLUS, "Add"},
{"##dl", TI_TRASH, "Delete"},
{"##dn", TI_CHEVRON_DOWN, "Dropdown"},
{"##cf", TI_SETTINGS, "Settings"},
{"##ok", TI_CHECK, "Check"},
{"##cl", TI_X, "Close"},
{"##ed", TI_PENCIL, "Edit"},
{"##sv", TI_DEVICE_FLOPPY, "Save"},
{"##sr", TI_SEARCH, "Search"},
{"##hp", TI_HELP, "Help"},
{"##hm", TI_HOME, "Home"},
};
for (auto& b : ic) {
icon_button(b.id, b.glyph, b.tip);
ImGui::SameLine();
}
ImGui::NewLine();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n"
" reload_data();\n\n"
"// Mas de 5000 iconos disponibles — ver core/icons_tabler.h"
);
}
// ---------------------------------------------------------------------------
// toolbar
// ---------------------------------------------------------------------------
void demo_toolbar() {
demo_header("toolbar", "v1.0.0",
"Grupo horizontal con spacing consistente y separadores verticales sutiles. "
"El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos.");
section("Example with two groups");
toolbar_begin();
button(TI_PLUS " New", V::Primary); ImGui::SameLine();
button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine();
button(TI_DEVICE_FLOPPY " Save",V::Secondary);
toolbar_separator();
icon_button("##set", TI_SETTINGS, "Settings");
ImGui::SameLine();
icon_button("##help", TI_HELP, "Help");
toolbar_end();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"toolbar_begin();\n"
" button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n"
" button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n"
" toolbar_separator();\n"
" icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n"
"toolbar_end();"
);
}
// ---------------------------------------------------------------------------
// modal_dialog
// ---------------------------------------------------------------------------
void demo_modal() {
demo_header("modal_dialog", "v1.0.0",
"Popup modal centrada con estilo surface+border. Close con Escape o click en X. "
"Patron begin/end — modal_dialog_end debe llamarse siempre.");
static bool show = false;
if (button("Open modal", V::Primary)) show = true;
if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) {
ImGui::TextWrapped(
"Modal centrada en el viewport principal, con estilo tokens.");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
static char buf[64] = {};
text_input("Name", buf, sizeof(buf), "escribe algo");
ImGui::Separator();
if (button("Cancel", V::Subtle)) show = false;
ImGui::SameLine();
if (button("Done", V::Primary)) show = false;
}
modal_dialog_end();
code_block(
"static bool show = false;\n"
"if (button(\"Open\", Primary)) show = true;\n"
"if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n"
" // ... campos del form ...\n"
" if (button(\"Done\", Primary)) show = false;\n"
"}\n"
"modal_dialog_end();"
);
}
// ---------------------------------------------------------------------------
// text_input
// ---------------------------------------------------------------------------
void demo_text_input() {
demo_header("text_input", "v1.0.0",
"Label muted + input estilizado con tokens. Full-width dentro del contenedor. "
"Placeholder opcional mostrado en text_dim cuando el buffer esta vacio.");
static char name[128] = {};
static char desc[256] = {};
static char tags[128] = {};
ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
text_input("Name", name, sizeof(name), "my-new-thing");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Description", desc, sizeof(desc));
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form");
ImGui::EndChild();
code_block(
"static char name[128] = {};\n"
"text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n"
"// true on change — se usa mas para validar en vivo\n"
"// que para leer el valor (que vive en el buffer)."
);
}
// ---------------------------------------------------------------------------
// select
// ---------------------------------------------------------------------------
void demo_select() {
demo_header("select", "v1.0.0",
"Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input.");
static int lang_idx = 0;
static int domain_idx = -1;
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
const char* domains[] = {"core", "infra", "finance", "datascience", "viz"};
ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
select("Language", &lang_idx, langs, 5);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
select("Domain (optional)", &domain_idx, domains, 5, true);
ImGui::EndChild();
code_block(
"static int lang = 0;\n"
"const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"select(\"Language\", &lang, langs, 5);"
);
}
// ---------------------------------------------------------------------------
// toast + inbox
// ---------------------------------------------------------------------------
void demo_toast() {
demo_header("toast", "v1.1.0",
"Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. "
"La campana muestra badge con no-leidos y popover con las ultimas 50.");
section("Trigger toasts");
if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast");
ImGui::SameLine();
if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed");
ImGui::SameLine();
if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something");
ImGui::SameLine();
if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason");
section("Inbox (bell with unread badge)");
toast_inbox_button("##inbox_demo");
code_block(
"toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n"
"toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n"
"// En la toolbar:\n"
"toast_inbox_button(\"##inbox\");\n\n"
"// Una vez por frame al final del render:\n"
"toast_render();"
);
}
// ---------------------------------------------------------------------------
// tree_view
// ---------------------------------------------------------------------------
void demo_tree_view() {
demo_header("tree_view", "v1.0.0",
"Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). "
"Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro.");
static std::string selected;
section("Projects (fake)");
ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders);
struct FakeProject { const char* id; const char* name; const char* apps[3]; };
const FakeProject projs[] = {
{"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}},
{"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}},
{"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}},
};
for (auto& p : projs) {
bool sel = (selected == p.id);
if (tree_branch_begin(p.id, p.name, sel)) {
if (tree_node_clicked()) selected = p.id;
for (int i = 0; i < 3 && p.apps[i]; i++) {
bool asel = (selected == p.apps[i]);
tree_leaf(p.apps[i], p.apps[i], asel);
if (tree_node_clicked()) selected = p.apps[i];
}
tree_branch_end();
} else if (tree_node_clicked()) {
selected = p.id;
}
}
ImGui::EndChild();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str());
ImGui::PopStyleColor();
code_block(
"static std::string sel;\n"
"if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n"
" if (tree_node_clicked()) sel = p.id;\n"
" for (auto& a : p.apps) {\n"
" tree_leaf(a.id, a.name, sel == a.id);\n"
" if (tree_node_clicked()) sel = a.id;\n"
" }\n"
" tree_branch_end();\n"
"}"
);
}
// ---------------------------------------------------------------------------
// kpi_card
// ---------------------------------------------------------------------------
void demo_kpi_card() {
demo_header("kpi_card", "v1.3.0",
"Card compacta 86px con icono opcional + label muted, valor x1.4, trend con "
"TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md.");
if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
float history[] = {10, 12, 11, 15, 18, 17, 20};
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS);
ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR);
ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE);
ImGui::EndTable();
}
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"float history[] = {10,12,11,15,18,17,20};\n"
"kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n"
"kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n"
"// Sin delta ni history: muestra TI_MINUS como placeholder\n"
"kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);"
);
}
// ---------------------------------------------------------------------------
// badge
// ---------------------------------------------------------------------------
void demo_badge() {
demo_header("badge", "v1.0.0",
"Etiqueta inline con 6 variantes semanticas. Equivalente a <Badge> de fn_library.");
section("Variants");
badge("Default", BadgeVariant::Default); ImGui::SameLine();
badge("Success", BadgeVariant::Success); ImGui::SameLine();
badge("Warning", BadgeVariant::Warning); ImGui::SameLine();
badge("Error", BadgeVariant::Error); ImGui::SameLine();
badge("Info", BadgeVariant::Info); ImGui::SameLine();
badge("Outline", BadgeVariant::Outline);
section("In context (table row)");
ImGui::Text("filter_slice_go_core"); ImGui::SameLine();
badge("pure", BadgeVariant::Success); ImGui::SameLine();
badge("tested", BadgeVariant::Info);
code_block(
"badge(\"pure\", BadgeVariant::Success);\n"
"badge(\"stale\", BadgeVariant::Warning);\n"
"badge(\"broken\", BadgeVariant::Error);"
);
}
// ---------------------------------------------------------------------------
// empty_state
// ---------------------------------------------------------------------------
void demo_empty_state() {
demo_header("empty_state", "v1.0.0",
"Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias.");
ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders);
empty_state("( no data )", "No projects yet",
"Create one under projects/{name}/ with project.md and reindex");
ImGui::EndChild();
code_block(
"if (apps.empty()) {\n"
" empty_state(\"( no data )\", \"No apps yet\",\n"
" \"Click + Add to create one\");\n"
" return;\n"
"}"
);
}
// ---------------------------------------------------------------------------
// page_header
// ---------------------------------------------------------------------------
void demo_page_header() {
demo_header("page_header", "v1.0.0",
"Header de pagina con titulo, subtitulo opcional y separador final. "
"Patron begin/end permite insertar acciones entre titulo y separador.");
page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses");
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f);
toolbar_begin();
button("Reload", V::Subtle); ImGui::SameLine();
button("+ Add", V::Secondary);
toolbar_end();
page_header_end();
code_block(
"page_header_begin(\"Dashboard\", subtitle);\n"
"ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n"
"toolbar_begin();\n"
" button(\"Reload\", Subtle);\n"
"toolbar_end();\n"
"page_header_end();"
);
}
// ---------------------------------------------------------------------------
// dashboard_panel
// ---------------------------------------------------------------------------
void demo_dashboard_panel() {
demo_header("dashboard_panel", "v1.0.0",
"Contenedor tipo panel con titulo, bordes redondeados, bg surface. "
"Auto-resize-Y segun contenido. Usa min_width/min_height como piso.");
if (dashboard_panel_begin("Revenue", 0, 120.0f)) {
ImGui::Text("Some panel content goes here.");
ImGui::Text("Anything drawn inside lives in the child window.");
}
dashboard_panel_end();
code_block(
"if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n"
" ImGui::Text(\"content\");\n"
"}\n"
"dashboard_panel_end();"
);
}
} // namespace gallery
@@ -1,215 +0,0 @@
// Demos faltantes: process_runner (Core), candlestick / gauge / heatmap /
// table_view (Viz). Aniade cobertura sobre los primitivos del registry que
// no tenian su entry en la gallery.
#include "demos.h"
#include "demo.h"
#include "core/process_runner.h"
#include "viz/candlestick.h"
#include "viz/gauge.h"
#include "viz/heatmap.h"
#include "viz/table_view.h"
#include <imgui.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <thread>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// process_runner (Core)
// ---------------------------------------------------------------------------
void demo_process_runner() {
demo_header("process_runner", "v1.0.0",
"Ejecuta una tarea en std::thread en background y expone estado thread-safe "
"(idle/running/success/error). El widget runner_status() dibuja inline un "
"spinner mientras corre y un mensaje de Success/Error al terminar.");
static fn_ui::ProcessRunner runner;
section("Tarea simulada (sleep 2s)");
{
if (ImGui::Button("Run task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(2));
out = "task done in 2s";
return true;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Run failing task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(1));
out = "simulated failure";
return false;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Reset")) runner.reset();
fn_ui::runner_status(runner, "Working...");
}
code_block(
"static fn_ui::ProcessRunner r;\n"
"if (button(\"Run\", Primary) && !r.is_busy()) {\n"
" fn_ui::runner_trigger(r, [](std::string& out) -> bool {\n"
" return do_work(&out);\n"
" });\n"
"}\n"
"fn_ui::runner_status(r, \"Working...\");"
);
}
// ---------------------------------------------------------------------------
// candlestick (Viz)
// ---------------------------------------------------------------------------
void demo_candlestick() {
demo_header("candlestick", "v1.0.0",
"Grafico de velas OHLC con ImPlot custom rendering. Verde si close >= open, "
"rojo si bajista. Tooltip al hover muestra OHLC del dia.");
section("OHLC sintetico (30 dias)");
{
static std::vector<double> dates, opens, closes, lows, highs;
if (dates.empty()) {
dates.reserve(30); opens.reserve(30); closes.reserve(30);
lows.reserve(30); highs.reserve(30);
double price = 100.0;
for (int i = 0; i < 30; ++i) {
double drift = std::sin(i * 0.4) * 1.2;
double o = price;
double c = price + drift + (i % 3 == 0 ? -0.6 : 0.4);
double l = std::min(o, c) - 0.8 - (i % 5) * 0.1;
double h = std::max(o, c) + 0.8 + (i % 4) * 0.1;
dates.push_back(double(i));
opens.push_back(o);
closes.push_back(c);
lows.push_back(l);
highs.push_back(h);
price = c;
}
}
candlestick("##cs", dates.data(), opens.data(), closes.data(),
lows.data(), highs.data(), int(dates.size()));
}
code_block(
"candlestick(\"##cs\", dates, opens, closes, lows, highs, n,\n"
" /*width_percent=*/0.25f, /*tooltip=*/true);"
);
}
// ---------------------------------------------------------------------------
// gauge (Viz)
// ---------------------------------------------------------------------------
void demo_gauge() {
demo_header("gauge", "v1.0.0",
"Indicador circular tipo velocimetro con ImGui DrawList. Color interpolado "
"verde -> amarillo -> rojo segun el valor normalizado.");
static float v_cpu = 0.32f, v_mem = 0.78f, v_gpu = 0.55f;
section("Tres gauges con sliders");
{
ImGui::SliderFloat("cpu", &v_cpu, 0.0f, 1.0f);
ImGui::SliderFloat("mem", &v_mem, 0.0f, 1.0f);
ImGui::SliderFloat("gpu", &v_gpu, 0.0f, 1.0f);
ImGui::Spacing();
ImGui::BeginGroup();
gauge("CPU", v_cpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("MEM", v_mem, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("GPU", v_gpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
}
code_block("gauge(\"CPU\", 0.32f, 0.0f, 1.0f, 60.0f);");
}
// ---------------------------------------------------------------------------
// heatmap (Viz)
// ---------------------------------------------------------------------------
void demo_heatmap() {
demo_header("heatmap", "v1.0.0",
"Mapa de calor 2D con ImPlot. Datos row-major. Util para correlation "
"matrices, attention maps, distribuciones 2D discretas.");
constexpr int R = 12;
constexpr int C = 12;
static float values[R * C] = {0};
static bool init = false;
if (!init) {
for (int r = 0; r < R; ++r) {
for (int c = 0; c < C; ++c) {
float dx = (c - C * 0.5f) / float(C);
float dy = (r - R * 0.5f) / float(R);
values[r * C + c] = std::exp(-(dx * dx + dy * dy) * 6.0f);
}
}
init = true;
}
section("Gaussian 12x12");
{
heatmap("##hm", values, R, C, 0.0f, 1.0f);
}
code_block(
"float values[R * C];\n"
"// fill row-major: values[r * C + c] = ...\n"
"heatmap(\"##hm\", values, R, C, /*min=*/0.0f, /*max=*/1.0f);"
);
}
// ---------------------------------------------------------------------------
// table_view (Viz)
// ---------------------------------------------------------------------------
void demo_table_view() {
demo_header("table_view", "v1.0.0",
"Tabla interactiva con sorting indicators y scroll usando la ImGui Tables API. "
"Headers + cells row-major. Util para dashboards y inspectores.");
section("Lenguajes del registry");
{
const char* headers[] = {"id", "lang", "domain", "purity"};
// 6 filas x 4 cols, row-major
const char* cells[] = {
"filter_slice_go_core", "go", "core", "pure",
"metabase_setup_py_infra", "py", "infra", "impure",
"rsync_deploy_bash_infra", "sh", "infra", "impure",
"button_cpp_core", "cpp", "core", "pure",
"gl_texture_load_cpp_gfx", "cpp", "gfx", "impure",
"audio_fft_cpp_core", "cpp", "core", "pure",
};
const int row_count = 6;
const int col_count = 4;
table_view("##tbl", headers, col_count, cells, row_count);
}
code_block(
"const char* headers[] = {\"id\", \"lang\", \"domain\"};\n"
"const char* cells[] = {/* row-major: r0c0,r0c1,r0c2, r1c0,... */};\n"
"table_view(\"##tbl\", headers, 3, cells, n_rows);"
);
}
} // namespace gallery
-196
View File
@@ -1,196 +0,0 @@
// Demos del dominio gfx — primitivos OpenGL/shader que viven en
// cpp/functions/gfx/. La pieza distintiva de shaders_lab es el
// shader_canvas: framebuffer + fullscreen quad + programa GL animado por
// time/resolution/mouse.
#include "demos.h"
#include "demo.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <chrono>
namespace gallery {
namespace {
// Fragment shader sintetico — gradiente animado con celdas. Usa los uniforms
// estandar que compile_fragment inyecta: u_resolution, u_time, u_mouse.
const char* kShaderSrc = R"(
void mainImage() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 cell = uv * 8.0;
vec2 ipos = floor(cell);
vec2 fpos = fract(cell) - 0.5;
float t = u_time * 0.6;
float wave = sin(ipos.x * 0.7 + ipos.y * 0.5 + t);
float dist = length(fpos);
vec3 a = vec3(0.30, 0.43, 0.96); // indigo
vec3 b = vec3(0.95, 0.45, 0.85); // pink
vec3 col = mix(a, b, 0.5 + 0.5 * wave);
// Mouse focus: oscurecemos celdas lejanas al cursor.
vec2 m = u_mouse / u_resolution;
float fm = 1.0 - smoothstep(0.0, 0.6, length(uv - m));
col *= 0.6 + 0.4 * fm;
// Disco interior por celda con borde suave.
col *= smoothstep(0.5, 0.45, dist);
fragColor = vec4(col, 1.0);
}
void main() {
mainImage();
}
)";
struct CanvasState {
fn::gfx::ShaderCanvas canvas;
bool compiled = false;
bool compile_failed = false;
std::string err_msg;
std::chrono::steady_clock::time_point t0;
};
CanvasState& state() {
static CanvasState s;
return s;
}
} // namespace
void demo_shader_canvas() {
demo_header("shader_canvas", "v1.0.0",
"Framebuffer + fullscreen quad + shader GLSL animado. La misma pieza "
"que usa shaders_lab para el preview en vivo. Uniforms u_time / u_resolution / u_mouse "
"los inyecta gl_shader::compile_fragment automaticamente.");
auto& s = state();
// Compilacion lazy (en el primer frame ya hay contexto GL valido).
if (!s.compiled && !s.compile_failed) {
fn::gfx::gl_loader_init();
fn::gfx::canvas_init(s.canvas);
auto cr = fn::gfx::compile_fragment(kShaderSrc);
if (!cr.ok) {
s.compile_failed = true;
s.err_msg = cr.err_msg;
} else {
fn::gfx::canvas_set_program(s.canvas, cr.program);
s.t0 = std::chrono::steady_clock::now();
s.compiled = true;
}
}
if (s.compile_failed) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1),
"Compilacion del fragment shader fallo:\n%s",
s.err_msg.c_str());
return;
}
section("Live preview");
// Render del shader en un panel ~480x300 px. canvas_render hace resize
// automatico segun GetContentRegionAvail si lo dejas crecer.
ImGui::BeginChild("##shader_preview", ImVec2(480, 300),
ImGuiChildFlags_Borders);
const float dt = std::chrono::duration<float>(
std::chrono::steady_clock::now() - s.t0).count();
fn::gfx::canvas_render(s.canvas, dt);
ImGui::EndChild();
code_block(
"#include \"gfx/shader_canvas.h\"\n"
"#include \"gfx/gl_shader.h\"\n\n"
"static fn::gfx::ShaderCanvas canvas;\n"
"// Setup (una vez):\n"
"fn::gfx::canvas_init(canvas);\n"
"auto cr = fn::gfx::compile_fragment(user_glsl);\n"
"if (cr.ok) fn::gfx::canvas_set_program(canvas, cr.program);\n\n"
"// Cada frame, dentro de un Begin/End:\n"
"fn::gfx::canvas_render(canvas, time_seconds);"
);
}
// Issue 0049b — Mostrar la version de OpenGL del contexto y un puñado de
// limites 4.3 que confirman que compute shaders / SSBO / image load-store
// estan disponibles. No es codigo del registry, solo introspeccion del
// driver — sin estado, sin side effects: solo glGetString + glGetIntegerv.
#ifndef GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD
#endif
#ifndef GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS
#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC
#endif
#ifndef GL_MAX_COMPUTE_SHARED_MEMORY_SIZE
#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
#endif
void demo_gl_info() {
demo_header("gl_info", "v1.0.0",
"Introspeccion del contexto OpenGL activo (issue 0049b). El framework "
"ahora pide GL 4.3 core, lo que habilita compute shaders, SSBOs, image "
"load/store y atomic counters — bloques esenciales del graph_renderer "
"GPU del proyecto osint_graph.");
auto gl_str = [](GLenum e) -> const char* {
const GLubyte* s = glGetString(e);
return s ? reinterpret_cast<const char*>(s) : "(null)";
};
section("Driver");
ImGui::Text("Vendor: %s", gl_str(GL_VENDOR));
ImGui::Text("Renderer: %s", gl_str(GL_RENDERER));
ImGui::Text("Version: %s", gl_str(GL_VERSION));
ImGui::Text("GLSL: %s", gl_str(GL_SHADING_LANGUAGE_VERSION));
GLint major = 0, minor = 0;
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
const bool has_43 = (major > 4) || (major == 4 && minor >= 3);
section("Capabilities");
ImGui::Text("Context: %d.%d core", major, minor);
if (has_43) {
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f),
"OpenGL 4.3+ — compute shaders, SSBOs, image load/store, atomic counters: AVAILABLE");
} else {
ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.30f, 1.0f),
"OpenGL < 4.3 — compute shaders / SSBOs NOT available on this driver");
}
section("Limits");
GLint v = 0;
auto row = [&](const char* label, GLenum e) {
v = 0;
glGetIntegerv(e, &v);
ImGui::Text("%-44s %d", label, v);
};
row("GL_MAX_TEXTURE_SIZE", GL_MAX_TEXTURE_SIZE);
row("GL_MAX_VERTEX_ATTRIBS", GL_MAX_VERTEX_ATTRIBS);
row("GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BLOCK_SIZE);
if (has_43) {
row("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
row("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS);
row("GL_MAX_COMPUTE_SHARED_MEMORY_SIZE", GL_MAX_COMPUTE_SHARED_MEMORY_SIZE);
}
code_block(
"// Solo glGetString + glGetIntegerv — sin loader extra.\n"
"GLint major = 0, minor = 0;\n"
"glGetIntegerv(GL_MAJOR_VERSION, &major);\n"
"glGetIntegerv(GL_MINOR_VERSION, &minor);\n"
"bool has_compute = (major > 4) || (major == 4 && minor >= 3);"
);
}
} // namespace gallery
@@ -1,127 +0,0 @@
// Demo de gl_texture_load (cpp/functions/gfx/gl_texture_load.{h,cpp}).
// Carga assets/sample.png y lo muestra con ImGui::Image. Sliders para tint
// RGB que se aplican como modulacion (ImGui::Image acepta tint_col).
//
// Limitacion: el "zoom UV" se simula moviendo uv0/uv1 (que ImGui::Image acepta
// nativamente). Asi evitamos compilar un shader custom adicional para la demo.
#include "demos.h"
#include "demo.h"
#include "gfx/gl_texture_load.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <cstdio>
#include <cstring>
namespace gallery {
namespace {
struct TextureState {
fn::GlTexture tex{};
bool tried_load = false;
std::string_view err;
char err_buf[256] = {0};
float tint[3] = {1.0f, 1.0f, 1.0f};
float zoom = 1.0f; // 1.0 = sin zoom; >1 hace UV mas pequeno
};
TextureState& state() {
static TextureState s;
return s;
}
// Resuelve un path para el asset. Probamos varios candidatos relativos al cwd
// del binario (puede lanzarse desde build/ o desde la raiz del repo).
const char* resolve_sample_path() {
static const char* candidates[] = {
"assets/sample.png",
"apps/primitives_gallery/assets/sample.png",
"cpp/apps/primitives_gallery/assets/sample.png",
"../cpp/apps/primitives_gallery/assets/sample.png",
"../../cpp/apps/primitives_gallery/assets/sample.png",
"../../../cpp/apps/primitives_gallery/assets/sample.png",
nullptr,
};
for (int i = 0; candidates[i]; i++) {
FILE* f = std::fopen(candidates[i], "rb");
if (f) { std::fclose(f); return candidates[i]; }
}
return candidates[0]; // devolver el primer candidato para que el error sea mas descriptivo
}
} // namespace
void demo_gl_texture() {
demo_header("gl_texture_load", "v1.0.0",
"Carga PNG/JPG/HDR desde disco a una textura GL lista para sampler2D. "
"Vendorea stb_image (cpp/vendor/stb/). Demo: assets/sample.png "
"(damero 256x256), tint RGB modulando ImGui::Image, zoom UV.");
auto& s = state();
if (!s.tried_load) {
// Asegurar simbolos GL resueltos (Linux no-op, Windows wglGetProcAddress).
fn::gfx::gl_loader_init();
const char* path = resolve_sample_path();
s.tex = fn::gl_texture_load(path, /*flip_y=*/true, /*srgb=*/false);
if (!s.tex.ok()) {
std::snprintf(s.err_buf, sizeof(s.err_buf),
"no se pudo cargar '%s': %s",
path, fn::gl_texture_last_error());
}
s.tried_load = true;
}
if (!s.tex.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", s.err_buf);
ImGui::TextWrapped(
"El binario busca el PNG en varios paths relativos al cwd. "
"Lanzar desde la raiz del repo o desde cpp/build/ deberia funcionar.");
return;
}
section("Texture info");
ImGui::Text("size: %d x %d px", s.tex.w, s.tex.h);
ImGui::Text("channels: %d (forzado a RGBA en upload)", s.tex.channels);
ImGui::Text("gl_id: %u", (unsigned)s.tex.id);
section("Tint + zoom");
ImGui::SliderFloat3("tint RGB", s.tint, 0.0f, 2.0f, "%.2f");
ImGui::SliderFloat("zoom UV", &s.zoom, 0.25f, 4.0f, "%.2fx");
section("Preview");
// Calcular UVs centradas con zoom: 1.0 = (0,0)-(1,1), 2.0 = (0.25,0.25)-(0.75,0.75)
float u_half = 0.5f / (s.zoom > 0.001f ? s.zoom : 0.001f);
ImVec2 uv0(0.5f - u_half, 0.5f - u_half);
ImVec2 uv1(0.5f + u_half, 0.5f + u_half);
ImVec4 tint(s.tint[0], s.tint[1], s.tint[2], 1.0f);
// Conversion GLuint -> ImTextureID. ImGui::Image acepta cualquier id de
// textura del backend; en imgui_impl_opengl3 es directamente el GLuint.
ImTextureID tid = (ImTextureID)(intptr_t)s.tex.id;
ImGui::ImageWithBg(tid, ImVec2(384.0f, 384.0f), uv0, uv1,
ImVec4(0, 0, 0, 0), tint);
code_block(
"#include \"gfx/gl_texture_load.h\"\n\n"
"auto tex = fn::gl_texture_load(\"assets/sample.png\");\n"
"if (!tex.ok()) {\n"
" fprintf(stderr, \"%s\\n\", fn::gl_texture_last_error());\n"
" return 1;\n"
"}\n"
"// uso en shader:\n"
"glUseProgram(prog);\n"
"fn::gl_texture_bind_uniform(prog, \"u_tex\", tex, /*unit=*/0);\n"
"glDrawArrays(GL_TRIANGLES, 0, 6);\n\n"
"// o en ImGui directamente:\n"
"ImGui::Image((ImTextureID)(intptr_t)tex.id, ImVec2(w, h));"
);
}
} // namespace gallery
-443
View File
@@ -1,443 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_force_layout_gpu.h"
#include "viz/graph_layouts.h"
#include "viz/graph_labels.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la
// tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo
// queda demostrado tal cual lo van a usar las apps reales (osint_graph,
// fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id.
static const uint32_t k_demo_palette[] = {
0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u,
0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u,
};
static constexpr int k_demo_palette_n =
sizeof(k_demo_palette) / sizeof(k_demo_palette[0]);
// Tabla compartida entre regeneraciones — las apariencias no cambian aunque
// el usuario regenere el grafo, asi que vive como `static`.
static EntityType s_demo_entity_types[k_demo_palette_n];
static RelationType s_demo_relation_types[1];
static bool s_demo_types_initialized = false;
static void init_demo_types() {
if (s_demo_types_initialized) return;
for (int k = 0; k < k_demo_palette_n; ++k) {
s_demo_entity_types[k] = entity_type(k_demo_palette[k],
SHAPE_CIRCLE, 4.0f, "cluster");
}
s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default");
s_demo_types_initialized = true;
}
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
// mismo cuadrado de 200 px — a 1M nodos crece a ~6 km de radio en graph
// space y los nodos pueden esparcirse libremente sin caja artificial.
static void generate_synthetic_graph(int N, int K,
int edges_per_node, int inter_pct,
std::vector<GraphNode>& nodes_out,
std::vector<GraphEdge>& edges_out) {
nodes_out.clear();
edges_out.clear();
nodes_out.reserve(N);
edges_out.reserve((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u);
unsigned seed = 0x1234abcd;
auto rnd = [&]() {
seed = seed * 1664525u + 1013904223u;
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
};
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
const float scale = std::sqrt(static_cast<float>(std::max(N, 1)));
const float cluster_r = 12.0f * scale;
const float scatter = 4.0f * scale;
std::vector<float> cluster_cx(K), cluster_cy(K);
for (int k = 0; k < K; k++) {
float angle = 2.0f * 3.14159f * k / K;
cluster_cx[k] = std::cos(angle) * cluster_r;
cluster_cy[k] = std::sin(angle) * cluster_r;
}
for (int i = 0; i < N; i++) {
int k = i % K;
// type_id mapea al EntityType (k % k_demo_palette_n) que define
// color y shape. size_override = 3..5 px para conservar la
// variacion sutil del demo v1 — apariencia visual identica.
uint16_t tid = static_cast<uint16_t>(k % k_demo_palette_n);
GraphNode n = graph_node(
cluster_cx[k] + (rnd() - 0.5f) * scatter,
cluster_cy[k] + (rnd() - 0.5f) * scatter,
tid);
n.size_override = 3.0f + rnd() * 2.0f;
n.user_data = static_cast<uint64_t>(i);
nodes_out.push_back(n);
}
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
if (a == b) return;
edges_out.push_back(graph_edge(a, b, w));
};
int per_cluster = N / K;
for (int k = 0; k < K; k++) {
int base = k * per_cluster;
int end = (k == K - 1) ? N : (base + per_cluster);
int size = end - base;
if (size < 2) continue;
for (int i = base; i < end; i++) {
for (int e = 0; e < edges_per_node; e++) {
int j = base + static_cast<int>(rnd() * size);
add_edge(static_cast<uint32_t>(i),
static_cast<uint32_t>(j), 1.0f);
}
}
}
// Inter-cluster: pct% del total de nodos
long long inter = (long long)N * (long long)inter_pct / 100LL;
for (long long e = 0; e < inter; e++) {
uint32_t a = static_cast<uint32_t>(rnd() * N);
uint32_t b = static_cast<uint32_t>(rnd() * N);
add_edge(a, b, 0.3f);
}
}
void demo_graph() {
demo_header("graph_viewport", "v1.0.0",
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
static int s_n_nodes = 1000;
static int s_n_clusters = 6;
static int s_edges_per_n = 3; // aristas intra-cluster por nodo
static int s_inter_pct = 5; // % de nodos para edges inter-cluster
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
static float s_attraction = 0.02f; // muelle entre nodos conectados
static float s_gravity = 0.001f; // tiron hacia el centro
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_needs_regen = true;
// GPU layout (issue 0049h): toggle CPU/GPU. ctx se crea perezosamente al
// primer frame en GPU mode; max_nodes/max_edges se dimensionan al maximo
// que ofrece el slider (1M nodos x 10 edges/nodo = 10M edges) — los SSBOs
// ocupan ~80 MB en ese tope, suficientemente barato para no
// recrear el ctx cada Regenerate. Si compute no esta disponible, el
// toggle queda deshabilitado.
static bool s_use_gpu = false;
static ForceLayoutGPU* s_gpu_ctx = nullptr;
static bool s_gpu_dirty = true; // re-upload tras regen / cambio
// Layout estatico activo (issue 0049i). 0=force (iterativo), 1=grid,
// 2=circular, 3=radial, 4=hierarchical, 5=fixed.
static int s_layout_mode = 0;
const char* k_layout_names[] = {
"force", "grid", "circular", "radial", "hierarchical", "fixed"
};
static int s_apply_layout = 0; // se incrementa cuando hay que reaplicar
// Labels (issue 0049j). LabelPolicy controlable desde la UI.
static graph::LabelPolicy s_label_policy;
static bool s_labels_enabled = true;
if (s_needs_regen) {
init_demo_types();
generate_synthetic_graph(s_n_nodes, s_n_clusters,
s_edges_per_n, s_inter_pct,
s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = static_cast<int>(s_nodes.size());
s_graph.node_capacity = static_cast<int>(s_nodes.capacity());
s_graph.edges = s_edges.data();
s_graph.edge_count = static_cast<int>(s_edges.size());
s_graph.edge_capacity = static_cast<int>(s_edges.capacity());
s_graph.types = s_demo_entity_types;
s_graph.type_count = k_demo_palette_n;
s_graph.rel_types = s_demo_relation_types;
s_graph.rel_type_count = 1;
s_graph.update_bounds();
s_state.layout_running = true;
s_state.layout_energy = 0.0f;
s_needs_regen = false;
s_initialized = true;
s_gpu_dirty = true;
}
section("Controls");
{
using namespace fn_ui;
ImGui::PushItemWidth(180);
// Slider Nodes con escala logaritmica para que sea util tanto a 100
// como a 1M sin tener que arrastrar 10000px.
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 1000000, "%d",
ImGuiSliderFlags_Logarithmic);
ImGui::SameLine();
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
ImGui::SliderInt("Edges/node", &s_edges_per_n, 1, 10);
ImGui::SameLine();
ImGui::SliderInt("Inter %", &s_inter_pct, 0, 30, "%d%%");
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
ImGui::SameLine();
ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f");
ImGui::PopItemWidth();
if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true;
ImGui::SameLine();
if (button(s_state.layout_running ? "Pause layout" : "Resume layout",
ButtonVariant::Secondary)) {
s_state.layout_running = !s_state.layout_running;
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
ImGui::SameLine();
// Toggle GPU layout. Si compute no esta disponible (Mesa software o
// driver < 4.3), deshabilitamos visualmente el checkbox.
bool prev_gpu = s_use_gpu;
if (s_gpu_ctx == nullptr && s_use_gpu == false) {
// primera oportunidad: intentar crear el ctx para detectar soporte.
// Lazy init solo si el usuario lo activa.
}
ImGui::Checkbox("GPU layout", &s_use_gpu);
if (s_use_gpu != prev_gpu) {
s_gpu_dirty = true; // re-upload al cambiar de modo
}
// Selector de layout (issue 0049i).
ImGui::PushItemWidth(140);
int prev_mode = s_layout_mode;
if (ImGui::Combo("Layout", &s_layout_mode,
k_layout_names, IM_ARRAYSIZE(k_layout_names))) {
// Cambio de modo: reaplicar instantaneamente
s_apply_layout++;
}
if (prev_mode != s_layout_mode) {
// En "force" volvemos a animar; en cualquier estatico paramos.
s_state.layout_running = (s_layout_mode == 0);
}
ImGui::PopItemWidth();
ImGui::SameLine();
if (button("Apply layout", ButtonVariant::Subtle)) s_apply_layout++;
// --- Labels (issue 0049j) ---------------------------------------
ImGui::Checkbox("Labels", &s_labels_enabled);
ImGui::SameLine();
ImGui::PushItemWidth(140);
ImGui::SliderInt("Max visible", &s_label_policy.max_visible, 0, 1000);
ImGui::SameLine();
ImGui::SliderFloat("Font", &s_label_policy.font_size,
8.0f, 24.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Min px", &s_label_policy.min_node_pixel_size,
0.0f, 40.0f, "%.0f");
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::Checkbox("Selected", &s_label_policy.always_for_selected);
ImGui::SameLine();
ImGui::Checkbox("Hovered", &s_label_policy.always_for_hovered);
ImGui::SameLine();
ImGui::Checkbox("Pinned", &s_label_policy.always_for_pinned);
}
section("Stats");
{
// Una sola linea fija — sin secciones condicionales que cambien la
// altura del panel (eso provocaba que el viewport saltara al hacer
// hover/select).
char hover_buf[32];
char sel_buf[32];
if (s_state.hovered_node >= 0) {
std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u",
s_state.hovered_node,
(unsigned)s_nodes[s_state.hovered_node].type_id);
} else {
std::snprintf(hover_buf, sizeof(hover_buf), "-");
}
if (s_state.selected_node >= 0) {
std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node);
} else {
std::snprintf(sel_buf, sizeof(sel_buf), "-");
}
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s",
s_graph.node_count, s_graph.edge_count,
s_state.layout_energy, ImGui::GetIO().Framerate,
hover_buf, sel_buf);
ImGui::PopStyleColor();
}
// Aplicar layout estatico cuando se solicita (cambio de modo / boton).
static int s_last_apply = -1;
if (s_apply_layout != s_last_apply) {
s_last_apply = s_apply_layout;
switch (s_layout_mode) {
case 1: graph::layout_grid (s_graph, 25.0f); break;
case 2: graph::layout_circular (s_graph, 200.0f); break;
case 3: graph::layout_radial (s_graph, 0, 80.0f); break;
case 4: graph::layout_hierarchical(s_graph, 0, 120.0f, 50.0f); break;
case 5: graph::layout_fixed (s_graph); break;
case 0: default:
// force: dejar las posiciones actuales; el bucle lo refinara
break;
}
s_gpu_dirty = true;
if (s_layout_mode != 0) graph_viewport_fit(s_graph, s_state);
}
section("Viewport (drag=pan, wheel=zoom, click=select, shift+drag=lasso, ctrl+click=toggle)");
if (s_initialized) {
// Avanzamos 1 paso de force layout cada frame mientras layout_running.
// Auto-pause: si la energia por nodo cae bajo el umbral durante N
// frames consecutivos, paramos la simulacion automaticamente — el
// grafo ya esta estable. El usuario lo retoma con "Resume layout"
// o "Regenerate".
static int s_low_energy_frames = 0;
const int k_pause_after_frames = 30;
const float k_pause_per_node = 0.001f; // umbral de energia/nodo
if (s_state.layout_running && s_layout_mode == 0) {
ForceLayoutConfig cfg;
cfg.repulsion = s_repulsion;
cfg.attraction = s_attraction;
cfg.gravity = s_gravity;
cfg.iterations = 1;
if (s_use_gpu) {
if (!s_gpu_ctx) {
s_gpu_ctx = graph_force_layout_gpu_create(s_graph.node_count + 1024,
s_graph.edge_count + 1024);
s_gpu_dirty = true;
}
if (s_gpu_ctx) {
if (s_gpu_dirty) {
graph_force_layout_gpu_upload(s_gpu_ctx, s_graph);
s_gpu_dirty = false;
}
s_state.layout_energy = graph_force_layout_gpu_step(s_gpu_ctx, cfg);
graph_force_layout_gpu_readback(s_gpu_ctx, s_graph, /*include_velocities=*/true);
} else {
// GPU no disponible: caer a CPU silenciosamente.
s_use_gpu = false;
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
} else {
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
const float per_node = s_graph.node_count > 0
? s_state.layout_energy / (float)s_graph.node_count
: 0.0f;
if (per_node < k_pause_per_node) ++s_low_energy_frames;
else s_low_energy_frames = 0;
if (graph_force_layout_should_pause(s_low_energy_frames,
k_pause_after_frames)) {
s_state.layout_running = false;
s_low_energy_frames = 0;
}
} else {
s_low_energy_frames = 0;
}
// Callbacks (issue 0049i): right-click abre popup contextual,
// double-click loguea el indice. Los callbacks corren dentro del
// frame ImGui — el caller puede usar OpenPopup directamente.
static int s_ctx_node = -1;
static bool s_ctx_open = false;
struct Cb {
static void on_ctx(int idx, ImVec2 /*pos*/, void* user) {
int* slot = (int*)user;
*slot = idx;
ImGui::OpenPopup("##graph_node_ctx");
}
static void on_dbl(int idx, void* /*user*/) {
std::printf("[graph] dbl-click on node %d\n", idx);
}
};
GraphViewportCallbacks cb;
cb.on_context_menu = &Cb::on_ctx;
cb.on_double_click = &Cb::on_dbl;
cb.user = &s_ctx_node;
graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460), cb);
// Labels overlay (issue 0049j). El callback formatea "#<idx>" en un
// buffer estatico por demo — apps reales (osint_graph) usaran un
// string pool de la BD origen.
if (s_labels_enabled) {
struct LblCtx { char buf[32]; };
static LblCtx s_lbl_ctx;
auto get_label = [](int idx, void* user) -> const char* {
auto* ctx = static_cast<LblCtx*>(user);
std::snprintf(ctx->buf, sizeof(ctx->buf), "#%d", idx);
return ctx->buf;
};
graph::graph_labels_draw(s_graph, s_state, s_label_policy,
get_label, &s_lbl_ctx);
}
if (ImGui::BeginPopup("##graph_node_ctx")) {
ImGui::Text("Node #%d", s_ctx_node);
ImGui::Separator();
if (ImGui::MenuItem("Pin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags |= NF_PINNED;
}
if (ImGui::MenuItem("Unpin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags &= ~NF_PINNED;
}
if (ImGui::MenuItem("Add to selection")) {
graph_viewport_add_to_selection(s_graph, s_state, s_ctx_node);
}
ImGui::EndPopup();
}
// Overlay con count seleccionados (lasso/multi-select feedback).
if (!s_state.selection.empty()) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
ImGui::Text("[%zu selected]", s_state.selection.size());
ImGui::PopStyleColor();
}
}
code_block(
"static GraphData graph;\n"
"static GraphViewportState state;\n"
"// ... rellenar graph.nodes / graph.edges ...\n"
"graph.update_bounds();\n"
"\n"
"// Por frame:\n"
"if (state.layout_running) {\n"
" ForceLayoutConfig cfg;\n"
" cfg.repulsion = 3500; cfg.gravity = 0.001f;\n"
" graph_force_layout_step(graph, cfg);\n"
"}\n"
"graph_viewport(\"##g\", graph, state, ImVec2(0, 460));"
);
}
} // namespace gallery
@@ -1,243 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_renderer.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_icons.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// 6 codepoints Tabler representativos para los 6 EntityTypes del demo. El
// orden coincide con `s_entity_types[i]`: cada tipo apunta a `icon_id = i+1`
// (las regiones del atlas son 1-indexed; 0 reservado para "sin icono").
static const uint16_t k_demo_codepoints[6] = {
0xEB4Du, // TI_USER
0xEAE5u, // TI_MAIL
0xEAB9u, // TI_GLOBE
0xEB09u, // TI_PHONE
0xEA4Fu, // TI_BUILDING
0xEA88u, // TI_DATABASE
};
static const uint32_t k_styles_palette[6] = {
0xFF6BCB77u, // verde — Person (circle)
0xFFFF6B6Bu, // rojo — Email (square)
0xFF4D96FFu, // azul — Domain (diamond)
0xFFFFC75Fu, // ambar — Phone (hex)
0xFFC780E8u, // morado — Org (triangle)
0xFF52CDF2u, // cyan — Database (rounded square)
};
static const char* k_styles_names[6] = {
"Person", "Email", "Domain", "Phone", "Organization", "Database"
};
static EntityType s_entity_types[6];
static RelationType s_relation_types[3]; // solid, dashed, dotted
static IconAtlas* s_atlas = nullptr;
static bool s_types_initialized = false;
static bool s_atlas_bound = false;
static void init_demo_types() {
if (s_types_initialized) return;
for (int i = 0; i < 6; ++i) {
EntityType t{};
t.color = k_styles_palette[i];
t.shape = (uint8_t)(SHAPE_CIRCLE + i); // 1..6 — uno por shape
t.icon_id = (uint16_t)(i + 1); // 1-based
t.default_size = 14.0f;
t.name = k_styles_names[i];
s_entity_types[i] = t;
}
s_relation_types[0] = relation_type(0xFFCCCCCCu, EDGE_SOLID, 1.5f, "knows");
s_relation_types[1] = relation_type(0xFFFFB870u, EDGE_DASHED, 1.5f, "uses");
s_relation_types[2] = relation_type(0xFF89E0FCu, EDGE_DOTTED, 1.5f, "owns");
s_types_initialized = true;
}
// 30 nodos posicionados en un anillo por tipo. Aristas: cada nodo conecta a
// sus dos vecinos (arc) y a un nodo "central" del cluster siguiente. Mezcla
// de directed/undirected para validar las flechas.
static void build_demo_graph(std::vector<GraphNode>& nodes,
std::vector<GraphEdge>& edges)
{
nodes.clear();
edges.clear();
const int per_type = 5;
const float ring_r = 80.0f;
const float type_r = 30.0f;
for (int t = 0; t < 6; ++t) {
float ang_t = (float)t * (2.0f * 3.14159265f / 6.0f);
float cx = std::cos(ang_t) * ring_r;
float cy = std::sin(ang_t) * ring_r;
for (int k = 0; k < per_type; ++k) {
float a = (float)k * (2.0f * 3.14159265f / per_type) + ang_t * 0.3f;
GraphNode n = graph_node(cx + std::cos(a) * type_r,
cy + std::sin(a) * type_r,
(uint16_t)t);
n.user_data = (uint64_t)nodes.size();
nodes.push_back(n);
}
}
auto idx = [&](int t, int k) { return (uint32_t)(t * per_type + k); };
for (int t = 0; t < 6; ++t) {
// Aristas intra-cluster (knows = solid, undirected).
for (int k = 0; k < per_type; ++k) {
int next_k = (k + 1) % per_type;
GraphEdge e = graph_edge(idx(t, k), idx(t, next_k), 1.0f, /*type_id=*/0);
edges.push_back(e);
}
// Inter-cluster: del nodo 0 del cluster t al nodo 0 del cluster t+1
// como "uses" (dashed, directed).
int t_next = (t + 1) % 6;
GraphEdge e1 = graph_edge(idx(t, 0), idx(t_next, 0), 1.0f, /*type_id=*/1);
e1.flags |= EF_DIRECTED;
edges.push_back(e1);
// Y otra inter-cluster mas larga al cluster +2 como "owns" (dotted,
// directed). Asi se ven las 3 estilos a la vez.
int t_far = (t + 2) % 6;
GraphEdge e2 = graph_edge(idx(t, 2), idx(t_far, 3), 0.6f, /*type_id=*/2);
e2.flags |= EF_DIRECTED;
edges.push_back(e2);
}
}
void demo_graph_styles() {
demo_header("graph_renderer (shapes + icons + arrows + edge styles)", "v1.5.0",
"OSINT-style: 6 EntityTypes, uno por shape (circle, square, diamond, hex, "
"triangle, rounded square) con icono Tabler en el centro. 3 RelationTypes "
"(solid/dashed/dotted) con flechas en los aristas EF_DIRECTED. Mismas dos "
"draw calls que el viewport normal (1 nodos + 1 aristas).");
init_demo_types();
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_run_layout = false;
if (!s_initialized) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.node_capacity = (int)s_nodes.capacity();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.edge_capacity = (int)s_edges.capacity();
s_graph.types = s_entity_types;
s_graph.type_count = 6;
s_graph.rel_types = s_relation_types;
s_graph.rel_type_count = 3;
s_graph.update_bounds();
s_state.layout_running = false; // queremos ver las shapes posicionadas, no el caos del force
s_state.zoom = 2.0f;
s_initialized = true;
}
section("Legend");
{
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
for (int i = 0; i < 6; ++i) {
ImGui::Text("%-13s shape=%d icon_id=%d color=#%06x",
k_styles_names[i],
(int)s_entity_types[i].shape,
(int)s_entity_types[i].icon_id,
(unsigned)(s_entity_types[i].color & 0x00FFFFFFu));
}
ImGui::Text("Edges: knows=solid, uses=dashed (directed), owns=dotted (directed)");
ImGui::PopStyleColor();
}
section("Controls");
{
using namespace fn_ui;
if (button(s_run_layout ? "Pause force layout" : "Run force layout",
ButtonVariant::Secondary)) {
s_run_layout = !s_run_layout;
s_state.layout_running = s_run_layout;
}
ImGui::SameLine();
if (button("Rebuild", ButtonVariant::Subtle)) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.update_bounds();
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
}
section("Viewport");
if (s_run_layout) {
ForceLayoutConfig cfg;
cfg.repulsion = 1500.0f;
cfg.attraction = 0.04f;
cfg.gravity = 0.005f;
cfg.iterations = 1;
graph_force_layout_step(s_graph, cfg);
}
// El viewport crea internamente el GraphRenderer. La primera vez que se
// dibuja el panel, el renderer existe — bindeamos el atlas justo despues.
graph_viewport("##graph_styles", s_graph, s_state, ImVec2(0, 460));
if (!s_atlas_bound && s_state.renderer) {
s_atlas = graph_icons_build(k_demo_codepoints, 6, 32);
if (s_atlas) {
graph_renderer_set_icon_atlas(s_state.renderer,
graph_icons_texture(s_atlas),
graph_icons_uv_table(s_atlas),
graph_icons_count(s_atlas));
s_atlas_bound = true;
} else {
// Sin atlas: marcamos como bound para no reintentar cada frame —
// el renderer simplemente pinta sin overlay de iconos.
s_atlas_bound = true;
}
}
code_block(
"// Build atlas con 6 codepoints Tabler\n"
"const uint16_t cps[] = {0xEB4D, 0xEAE5, 0xEAB9, 0xEB09, 0xEA4F, 0xEA88};\n"
"IconAtlas* atlas = graph_icons_build(cps, 6, 32);\n"
"\n"
"// EntityTypes: cada uno con su shape e icono\n"
"EntityType person = {0xFF6BCB77, SHAPE_CIRCLE, /*icon_id=*/1, 14, \"Person\"};\n"
"EntityType email = {0xFFFF6B6B, SHAPE_SQUARE, /*icon_id=*/2, 14, \"Email\"};\n"
"// ... etc\n"
"\n"
"// RelationTypes: solid / dashed / dotted\n"
"RelationType knows = relation_type(0xFFCCCCCC, EDGE_SOLID, 1.5f, \"knows\");\n"
"RelationType uses = relation_type(0xFFFFB870, EDGE_DASHED, 1.5f, \"uses\");\n"
"\n"
"// Bind atlas al renderer\n"
"graph_renderer_set_icon_atlas(renderer, graph_icons_texture(atlas),\n"
" graph_icons_uv_table(atlas),\n"
" graph_icons_count(atlas));\n"
"\n"
"// Aristas direccionales\n"
"GraphEdge e = graph_edge(src, tgt, 1.0f, /*type_id=*/1);\n"
"e.flags |= EF_DIRECTED;");
}
} // namespace gallery
-108
View File
@@ -1,108 +0,0 @@
// Demo del primitivo viz/mesh_viewer.
// Genera un cubo procedural in-line, lo sube al GPU, y permite cargar un
// .obj desde un path ingresado en un text input.
#include "demos.h"
#include "demo.h"
#include "viz/mesh_viewer.h"
#include "gfx/mesh_obj_load.h"
#include "gfx/mesh_gpu.h"
#include "core/orbit_camera.h"
#include <imgui.h>
#include <cstring>
#include <string>
namespace gallery {
namespace {
const char* kCubeObj =
"v -1 -1 -1\nv 1 -1 -1\nv 1 1 -1\nv -1 1 -1\n"
"v -1 -1 1\nv 1 -1 1\nv 1 1 1\nv -1 1 1\n"
"f 4 3 2 1\n" // back (-Z) — winding for outward normal
"f 5 6 7 8\n" // front (+Z)
"f 1 2 6 5\n" // bottom (-Y)
"f 8 7 3 4\n" // top (+Y)
"f 5 8 4 1\n" // left (-X)
"f 2 3 7 6\n"; // right (+X)
struct State {
fn::gfx::MeshGpu mesh{};
fn::core::OrbitCamera cam{};
char path[512] = "";
std::string status;
bool wireframe = false;
bool initialized = false;
};
State& state() {
static State s;
return s;
}
void load_cube() {
auto& s = state();
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
auto cpu = fn::gfx::mesh_obj_parse(kCubeObj, std::strlen(kCubeObj));
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded cube: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "cube upload failed";
}
void load_from_path() {
auto& s = state();
if (!s.path[0]) { s.status = "path is empty"; return; }
auto cpu = fn::gfx::mesh_obj_load(s.path);
if (cpu.positions.empty()) { s.status = "parse/read failed"; return; }
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "upload failed";
}
} // namespace
void demo_mesh_viewer() {
demo_header("mesh_viewer", "v1.0.0",
"Visualizador 3D para inspeccion de geometria. Composicion de "
"mesh_obj_load (parser .obj puro) + mesh_gpu (upload VAO/VBO/EBO) + "
"orbit_camera (drag/wheel) + mesh_viewer (FBO + ImGui::Image + Lambert).");
auto& s = state();
if (!s.initialized) {
load_cube();
s.initialized = true;
}
// Controls row.
if (ImGui::Button("Reload cube")) load_cube();
ImGui::SameLine();
ImGui::Checkbox("Wireframe", &s.wireframe);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::SetNextItemWidth(360);
ImGui::InputTextWithHint("##obj_path", "absolute path to .obj", s.path, sizeof(s.path));
ImGui::SameLine();
if (ImGui::Button("Load .obj")) load_from_path();
ImGui::TextDisabled("status: %s | tris: %d | drag to orbit, wheel to zoom",
s.status.c_str(),
s.mesh.ok() ? s.mesh.index_count / 3 : 0);
ImGui::Separator();
fn::viz::MeshViewerConfig cfg{};
cfg.mesh = &s.mesh;
cfg.cam = &s.cam;
cfg.size = ImVec2(-1.0f, 480.0f);
cfg.color = IM_COL32(160, 200, 255, 255);
cfg.wireframe = s.wireframe;
fn::viz::mesh_viewer("##gallery_mesh_viewer", cfg);
}
} // namespace gallery
@@ -1,208 +0,0 @@
// demos_scientific.cpp — demos para los 5 charts cientificos del issue 0034:
// treemap, sankey, chord, contour, voronoi.
#include "demos.h"
#include "demo.h"
#include "viz/treemap.h"
#include "viz/sankey.h"
#include "viz/chord.h"
#include "viz/contour.h"
#include "viz/voronoi.h"
#include <imgui.h>
#include <cmath>
#include <cstdlib>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// treemap
// ---------------------------------------------------------------------------
void demo_treemap() {
demo_header("treemap", "v1.0.0",
"Squarified treemap (Bruls et al.) para jerarquias planas con valores. "
"Algoritmo puro separado del render.");
section("Gastos por categoria");
{
std::vector<TreemapItem> items = {
{"vivienda", 950.0f, IM_COL32(180, 120, 200, 255)},
{"comida", 320.0f, IM_COL32(120, 180, 200, 255)},
{"transporte", 180.0f, IM_COL32(200, 180, 120, 255)},
{"ocio", 140.0f, IM_COL32(200, 120, 160, 255)},
{"salud", 90.0f, IM_COL32(120, 200, 160, 255)},
{"otros", 60.0f, IM_COL32(160, 160, 200, 255)},
};
treemap("##gastos", items, ImVec2(-1, 320));
}
code_block(
"std::vector<TreemapItem> items = {\n"
" {\"vivienda\", 950.0f, IM_COL32(180,120,200,255)},\n"
" {\"comida\", 320.0f, IM_COL32(120,180,200,255)},\n"
" ...\n"
"};\n"
"treemap(\"##gastos\", items, ImVec2(-1, 320));"
);
}
// ---------------------------------------------------------------------------
// sankey
// ---------------------------------------------------------------------------
void demo_sankey() {
demo_header("sankey", "v1.0.0",
"Sankey diagram para flujos source -> target. BFS topologico para columnas, "
"bandas curvas (bezier cubico) para los links. Asume DAG.");
section("Clientes -> productos -> categorias");
{
std::vector<SankeyNode> nodes = {
{"premium"}, {"basicos"},
{"laptops"}, {"phones"}, {"tablets"},
{"hardware"}, {"software"}, {"servicios"},
};
std::vector<SankeyLink> links = {
// clientes -> productos
{0, 2, 80}, {0, 3, 30}, {0, 4, 15},
{1, 3, 60}, {1, 4, 40}, {1, 2, 20},
// productos -> categorias
{2, 5, 70}, {2, 6, 30},
{3, 5, 50}, {3, 7, 40},
{4, 6, 35}, {4, 7, 20},
};
sankey("##flow", nodes, links, ImVec2(-1, 360));
}
code_block(
"std::vector<SankeyNode> nodes = {{\"premium\"}, {\"basicos\"}, ...};\n"
"std::vector<SankeyLink> links = {{0, 2, 80}, {0, 3, 30}, ...};\n"
"sankey(\"##flow\", nodes, links, ImVec2(-1, 360));"
);
}
// ---------------------------------------------------------------------------
// chord
// ---------------------------------------------------------------------------
void demo_chord() {
demo_header("chord", "v1.0.0",
"Chord diagram para matrices NxN. Arcos proporcionales a sum(row) + cuerdas "
"internas con bezier cubico.");
section("Flujos entre paises (matriz 6x6 simetrica)");
{
constexpr int N = 6;
// simetrica de "comercio" entre 6 paises
static float M[N * N] = {
0, 10, 6, 12, 4, 3,
10, 0, 14, 3, 8, 2,
6, 14, 0, 9, 11, 5,
12, 3, 9, 0, 7, 6,
4, 8, 11, 7, 0, 13,
3, 2, 5, 6, 13, 0,
};
static const char* labels[N] = {"ESP", "FRA", "ITA", "DEU", "PRT", "GBR"};
chord("##chord", M, N, labels, ImVec2(420, 420));
}
code_block(
"float M[N*N] = { // simetrica\n"
" 0, 10, 6, 12, 4, 3,\n"
" 10, 0, 14, 3, 8, 2,\n"
" ...\n"
"};\n"
"const char* labels[6] = {\"ESP\",\"FRA\",\"ITA\",\"DEU\",\"PRT\",\"GBR\"};\n"
"chord(\"##c\", M, 6, labels);"
);
}
// ---------------------------------------------------------------------------
// contour
// ---------------------------------------------------------------------------
void demo_contour() {
demo_header("contour", "v1.0.0",
"Contour plot 2D via marching squares. Para una gaussiana centrada los "
"contornos resultantes son aproximadamente concentricos.");
constexpr int N = 32;
static float grid[N * N];
static bool init = false;
if (!init) {
// Mezcla de 2 gaussianas (peak central + secundario)
for (int y = 0; y < N; y++) {
for (int x = 0; x < N; x++) {
float dx1 = x - N * 0.45f, dy1 = y - N * 0.5f;
float dx2 = x - N * 0.75f, dy2 = y - N * 0.3f;
float v = std::exp(-(dx1 * dx1 + dy1 * dy1) / 70.0f)
+ 0.55f * std::exp(-(dx2 * dx2 + dy2 * dy2) / 30.0f);
grid[y * N + x] = v;
}
}
init = true;
}
static const float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};
contour("##gauss", grid, N, N, levels, 5, ImVec2(-1, 320));
code_block(
"constexpr int N = 32;\n"
"float grid[N*N];\n"
"for (int y = 0; y < N; y++)\n"
" for (int x = 0; x < N; x++) {\n"
" float dx = x - N/2.0f, dy = y - N/2.0f;\n"
" grid[y*N + x] = std::exp(-(dx*dx + dy*dy) / 80.0f);\n"
" }\n"
"float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};\n"
"contour(\"##gauss\", grid, N, N, levels, 5);"
);
}
// ---------------------------------------------------------------------------
// voronoi
// ---------------------------------------------------------------------------
void demo_voronoi() {
demo_header("voronoi", "v1.0.0",
"Diagrama de Voronoi via raster brute-force (MVP). Tiles 4x4 px coloreados "
"por el seed mas cercano. Suficiente para N <= 200.");
constexpr int N = 30;
static ImVec2 seeds [N];
static ImU32 colors[N];
static bool init = false;
if (!init) {
unsigned seed = 7;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return (float)((seed >> 16) & 0x7fff) / 32768.0f;
};
// El render escala automaticamente; las posiciones se asumen en coords del rect.
// Como no sabemos W/H aqui, usamos coords aproximadas para 600x300 y el clip
// dentro de voronoi se encarga de mantenerlas en rango.
for (int i = 0; i < N; i++) {
seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);
colors[i] = IM_COL32(40 + (int)(rnd() * 200),
40 + (int)(rnd() * 200),
60 + (int)(rnd() * 195),
230);
}
init = true;
}
voronoi("##v", seeds, N, colors, ImVec2(-1, 300));
code_block(
"ImVec2 seeds[30];\n"
"ImU32 colors[30];\n"
"for (int i = 0; i < 30; i++) {\n"
" seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);\n"
" colors[i] = IM_COL32(rnd_byte(), rnd_byte(), rnd_byte(), 230);\n"
"}\n"
"voronoi(\"##v\", seeds, 30, colors, ImVec2(-1, 300));"
);
}
} // namespace gallery
-129
View File
@@ -1,129 +0,0 @@
// Demo de sql_workbench (Core, issue 0032).
//
// Abre `registry.db` en modo readonly y deja que el componente liste sus
// tablas en la sidebar. La idea es probar el ciclo Run + tabla + historial
// contra una DB real sin riesgo de mutarla.
#include "demos.h"
#include "demo.h"
#include "core/sql_workbench.h"
#include "core/tokens.h"
#include <imgui.h>
#include <sqlite3.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace gallery {
namespace {
struct SqlDemoState {
sqlite3* db = nullptr;
fn::SqlWorkbenchState wb;
bool tried_open = false;
std::string db_path;
std::string open_error;
};
SqlDemoState& state() {
static SqlDemoState s;
return s;
}
// Resuelve la ruta a registry.db: env FN_REGISTRY_ROOT/registry.db si existe,
// si no, prueba ./registry.db, ../registry.db, ../../registry.db (build tree).
std::string resolve_registry_db() {
if (const char* env = std::getenv("FN_REGISTRY_ROOT")) {
std::string p = std::string(env) + "/registry.db";
if (FILE* f = std::fopen(p.c_str(), "rb")) { std::fclose(f); return p; }
}
const char* candidates[] = {
"registry.db",
"../registry.db",
"../../registry.db",
"../../../registry.db",
"../../../../registry.db",
};
for (const char* c : candidates) {
if (FILE* f = std::fopen(c, "rb")) { std::fclose(f); return c; }
}
return "";
}
void ensure_open() {
auto& s = state();
if (s.tried_open) return;
s.tried_open = true;
s.db_path = resolve_registry_db();
if (s.db_path.empty()) {
s.open_error = "registry.db not found (tried FN_REGISTRY_ROOT and parent dirs)";
return;
}
int rc = sqlite3_open_v2(s.db_path.c_str(), &s.db,
SQLITE_OPEN_READONLY, nullptr);
if (rc != SQLITE_OK) {
s.open_error = sqlite3_errmsg(s.db);
if (s.db) { sqlite3_close(s.db); s.db = nullptr; }
return;
}
s.wb.readonly = true;
// Query inicial mas util para el demo: lista de funciones del registry.
s.wb.query =
"SELECT id, kind, purity, domain\n"
"FROM functions\n"
"ORDER BY id\n"
"LIMIT 50;";
}
} // namespace
void demo_sql_workbench() {
using namespace fn_tokens;
demo_header("sql_workbench", "v1.0.0",
"Workbench SQL: editor con highlighting, schema sidebar, tabla de "
"resultados e historial. Ejecuta queries contra una sqlite3* del caller. "
"En este demo, registry.db abierto en modo readonly.");
ensure_open();
auto& s = state();
if (!s.open_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("could not open registry.db: %s", s.open_error.c_str());
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("Set FN_REGISTRY_ROOT to the repo root or run from the repo cwd.");
ImGui::PopStyleColor();
return;
}
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text("db: %s (readonly)", s.db_path.c_str());
ImGui::PopStyleColor();
section("workbench");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
// Reserva un pelin para el code_block de abajo.
float h = avail.y - 110.0f;
if (h < 320.0f) h = 320.0f;
fn::sql_workbench("##gallery_sql", s.db, s.wb, ImVec2(-1, h));
}
code_block(
"sqlite3* db = nullptr;\n"
"sqlite3_open_v2(\"registry.db\", &db, SQLITE_OPEN_READONLY, nullptr);\n"
"fn::SqlWorkbenchState st;\n"
"st.readonly = true;\n"
"fn::sql_workbench(\"##sql\", db, st, ImVec2(-1, -1));"
);
}
} // namespace gallery
@@ -1,279 +0,0 @@
// Demos individuales de text_editor y file_watcher (Wave 1, issue 0025).
//
// Aunque las dos primitivas estan diseñadas para componerse, en gallery se
// muestran por separado para que cada entry exhiba un solo primitivo y su
// API minima.
#include "demos.h"
#include "demo.h"
#include "core/text_editor.h"
#include "core/file_watcher.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <deque>
#include <string>
namespace gallery {
// ===========================================================================
// text_editor — editor de codigo con syntax highlighting
// ===========================================================================
namespace {
const char* kSampleGLSL =
"#version 330\n"
"// fragment shader demo\n"
"out vec4 frag_color;\n"
"uniform vec2 u_resolution;\n"
"uniform float u_time;\n"
"\n"
"void main() {\n"
" vec2 uv = gl_FragCoord.xy / u_resolution;\n"
" vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0,2,4));\n"
" frag_color = vec4(col, 1.0);\n"
"}\n";
const char* kSampleSQL =
"-- fts5 search sobre el registry\n"
"SELECT id, kind, purity, description\n"
"FROM functions\n"
"WHERE id IN (\n"
" SELECT id FROM functions_fts\n"
" WHERE functions_fts MATCH 'name:slic* OR description:slic*'\n"
")\n"
"ORDER BY name\n"
"LIMIT 50;\n";
const char* kSampleCpp =
"#include <imgui.h>\n"
"namespace fn {\n"
" bool button(const char* label, ButtonVariant v) {\n"
" auto& tk = tokens::current();\n"
" ImGui::PushStyleColor(ImGuiCol_Button, tk.bg_for(v));\n"
" bool clicked = ImGui::Button(label);\n"
" ImGui::PopStyleColor();\n"
" return clicked;\n"
" }\n"
"}\n";
struct EditorState {
fn::TextEditorState* ed = nullptr;
fn::CodeLang lang = fn::CodeLang::GLSL;
};
EditorState& editor_state() {
static EditorState s;
return s;
}
void ensure_editor() {
auto& s = editor_state();
if (!s.ed) {
s.ed = fn::text_editor_create(s.lang);
fn::text_editor_set_text(s.ed, kSampleGLSL);
}
}
void apply_language(fn::CodeLang next) {
auto& s = editor_state();
if (next == s.lang) return;
fn::text_editor_destroy(s.ed);
s.ed = fn::text_editor_create(next);
s.lang = next;
switch (next) {
case fn::CodeLang::GLSL: fn::text_editor_set_text(s.ed, kSampleGLSL); break;
case fn::CodeLang::SQL: fn::text_editor_set_text(s.ed, kSampleSQL); break;
case fn::CodeLang::Cpp: fn::text_editor_set_text(s.ed, kSampleCpp); break;
case fn::CodeLang::Generic: fn::text_editor_set_text(s.ed, ""); break;
}
}
} // namespace
void demo_text_editor() {
using namespace fn_tokens;
demo_header("text_editor", "v1.0.0",
"Editor de codigo embebido en ImGui con syntax highlighting (GLSL/SQL/Cpp/Generic). "
"Wrapper PIMPL sobre ImGuiColorTextEdit (MIT). API: create / set_text / get_text / "
"render / is_dirty.");
ensure_editor();
auto& s = editor_state();
section("language");
{
const char* labels[] = {"GLSL", "SQL", "Cpp", "Generic"};
const fn::CodeLang langs[] = {
fn::CodeLang::GLSL, fn::CodeLang::SQL, fn::CodeLang::Cpp, fn::CodeLang::Generic
};
for (int i = 0; i < 4; ++i) {
if (i > 0) ImGui::SameLine();
bool active = (s.lang == langs[i]);
if (active) ImGui::PushStyleColor(ImGuiCol_Button, colors::primary);
if (ImGui::Button(labels[i])) apply_language(langs[i]);
if (active) ImGui::PopStyleColor();
}
}
section("editor");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
float h = avail.y - 60.0f;
if (h < 220.0f) h = 220.0f;
fn::text_editor_render(s.ed, "##fn_text_editor_solo", ImVec2(-1, h));
if (fn::text_editor_is_dirty(s.ed)) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::warning);
ImGui::TextUnformatted("(modified)");
ImGui::PopStyleColor();
ImGui::SameLine();
if (ImGui::Button("clear dirty##te_solo")) fn::text_editor_clear_dirty(s.ed);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("(clean)");
ImGui::PopStyleColor();
}
}
code_block(
"auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);\n"
"fn::text_editor_set_text(ed, src);\n"
"if (fn::text_editor_render(ed, \"##ed\", {600, 400}))\n"
" on_changed(fn::text_editor_get_text(ed));"
);
}
// ===========================================================================
// file_watcher — watcher cross-platform no bloqueante
// ===========================================================================
namespace {
constexpr const char* kWatchPath = "/tmp/fn_demo.glsl";
struct WatcherDemoState {
fn::FileWatcher* fw = nullptr;
bool active = false;
std::string err;
std::deque<std::string> events;
};
WatcherDemoState& watcher_state() {
static WatcherDemoState s;
return s;
}
void ensure_watcher() {
auto& s = watcher_state();
if (!s.fw) {
s.fw = fn::file_watcher_create();
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
const char* kind_label(fn::FileEvent::Kind k) {
switch (k) {
case fn::FileEvent::Modified: return "MODIFIED";
case fn::FileEvent::Created: return "CREATED";
case fn::FileEvent::Deleted: return "DELETED";
}
return "?";
}
void poll_and_log() {
auto& s = watcher_state();
if (!s.fw) return;
auto evs = fn::file_watcher_poll(s.fw);
for (auto& e : evs) {
char buf[512];
std::snprintf(buf, sizeof(buf), "[%s] %s", kind_label(e.kind), e.path.c_str());
s.events.push_back(buf);
}
while (s.events.size() > 200) s.events.pop_front();
}
bool touch_demo_file(std::string& err_out) {
FILE* f = std::fopen(kWatchPath, "a");
if (!f) { err_out = std::strerror(errno); return false; }
std::fprintf(f, "// touch %ld\n", (long)std::time(nullptr));
std::fclose(f);
return true;
}
} // namespace
void demo_file_watcher() {
using namespace fn_tokens;
demo_header("file_watcher", "v1.0.0",
"Watcher de archivos cross-platform no bloqueante. Linux: inotify. Windows: "
"ReadDirectoryChangesW. API: create / add / poll (drain) / destroy. Cap del "
"buffer de eventos: 200.");
ensure_watcher();
poll_and_log();
auto& s = watcher_state();
section("watcher state");
ImGui::Text("path: %s", kWatchPath);
ImGui::Text("active: %s", s.active ? "yes" : "no");
if (!s.err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("err: %s", s.err.c_str());
ImGui::PopStyleColor();
}
section("trigger events");
{
if (ImGui::Button("touch (append timestamp)")) {
std::string e;
if (!touch_demo_file(e)) s.err = "touch failed: " + e;
else s.err.clear();
// Si el archivo no existia al inicio, reintenta el add.
if (!s.active) {
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
ImGui::SameLine();
if (ImGui::Button("clear events")) s.events.clear();
ImGui::SameLine();
ImGui::TextDisabled("(o desde otro terminal: echo hi >> %s)", kWatchPath);
}
section("event log");
ImGui::Text("captured: %d", (int)s.events.size());
ImGui::BeginChild("##fw_evlog", ImVec2(0, 0), ImGuiChildFlags_Borders);
if (s.events.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextWrapped("Sin eventos. Pulsa touch o modifica el path desde otro terminal.");
ImGui::PopStyleColor();
} else {
for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) {
ImGui::TextUnformatted(it->c_str());
}
}
ImGui::EndChild();
code_block(
"auto* fw = fn::file_watcher_create();\n"
"fn::file_watcher_add(fw, \"/tmp/foo.glsl\");\n"
"for (auto& e : fn::file_watcher_poll(fw)) {\n"
" handle_event(e.path, e.kind);\n"
"}"
);
}
} // namespace gallery
-211
View File
@@ -1,211 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/bar_chart.h"
#include "viz/pie_chart.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/histogram.h"
#include "viz/sparkline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// bar_chart
// ---------------------------------------------------------------------------
void demo_bar_chart() {
demo_header("bar_chart", "v1.2.0",
"Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados "
"de labels cuando no caben horizontalmente.");
section("Labels que caben horizontalmente");
{
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f};
bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f);
}
section("Labels largos que obligan a rotar");
{
const char* domains[] = {
"core", "infrastructure", "finance", "datascience",
"cybersecurity", "notebook", "browser"
};
float values[] = {412, 187, 94, 63, 42, 38, 22};
bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f);
}
code_block(
"const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"float values[] = {412,187,94,63,36};\n"
"bar_chart(\"##lang\", labels, values, 5); // h=200 default\n"
"bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura"
);
}
// ---------------------------------------------------------------------------
// pie_chart
// ---------------------------------------------------------------------------
void demo_pie_chart() {
demo_header("pie_chart", "v1.1.0",
"Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con "
"valor absoluto + porcentaje.");
if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
{
const char* labels[] = {"Pure", "Impure"};
float values[] = {412.0f, 278.0f};
variant_label("Pie (radius auto)");
pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f);
}
ImGui::TableSetColumnIndex(1);
{
const char* labels[] = {"function", "pipeline", "component"};
float values[] = {618.0f, 42.0f, 230.0f};
variant_label("Donut (radius = -0.45)");
pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f);
}
ImGui::EndTable();
}
code_block(
"const char* labels[] = {\"Pure\",\"Impure\"};\n"
"float values[] = {412, 278};\n"
"pie_chart(\"##p\", labels, values, 2); // pie auto\n"
"pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut"
);
}
// ---------------------------------------------------------------------------
// line_plot
// ---------------------------------------------------------------------------
void demo_line_plot() {
demo_header("line_plot", "v1.1.0",
"Line plot 2D con limites de ejes calculados de min/max y pineados. "
"Sin auto-fit animado, sin pan/zoom.");
constexpr int N = 100;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.1f;
ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f);
}
init = true;
}
line_plot("##line", xs, ys, N, 240.0f);
code_block(
"line_plot(\"##series\", xs, ys, count); // h=200 default\n"
"line_plot(\"##series\", xs, ys, count, 300.0f); // custom height"
);
}
// ---------------------------------------------------------------------------
// scatter_plot
// ---------------------------------------------------------------------------
void demo_scatter_plot() {
demo_header("scatter_plot", "v1.1.0",
"Puntos dispersos con ejes pineados (5% headroom). Sin interaccion.");
constexpr int N = 120;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
unsigned seed = 1234;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
for (int i = 0; i < N; i++) {
xs[i] = rnd() * 10.0f;
ys[i] = 0.5f * xs[i] + rnd() * 3.0f;
}
init = true;
}
scatter_plot("##sc", xs, ys, N, 240.0f);
code_block(
"scatter_plot(\"##xy\", xs, ys, count, 240.0f);"
);
}
// ---------------------------------------------------------------------------
// histogram
// ---------------------------------------------------------------------------
void demo_histogram() {
demo_header("histogram", "v1.1.0",
"Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit "
"para los bins + Lock para bloquear pan/zoom.");
constexpr int N = 300;
static float vals[N];
static bool init = false;
if (!init) {
unsigned seed = 42;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
// Aproximacion de distribucion normal via box-muller simplificado
for (int i = 0; i < N; i++) {
float u1 = rnd() + 1e-6f;
float u2 = rnd();
vals[i] = std::sqrt(-2.0f * std::log(u1))
* std::cos(2.0f * 3.14159f * u2);
}
init = true;
}
histogram("##hist", vals, N, -1, 240.0f);
code_block(
"histogram(\"##h\", values, count); // bins=Sturges\n"
"histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300"
);
}
// ---------------------------------------------------------------------------
// sparkline
// ---------------------------------------------------------------------------
void demo_sparkline() {
demo_header("sparkline", "v1.0.0",
"Mini grafico de lineas inline (rellenado con alpha + linea). "
"Pensado para tablas, KPI cards, headers.");
float up[] = {10, 12, 11, 15, 18, 17, 20};
float down[] = {30, 28, 29, 25, 22, 24, 20};
float flat[] = {10, 10, 10, 10, 10, 10, 10};
ImGui::Text("Trending up "); ImGui::SameLine();
sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Trending down"); ImGui::SameLine();
sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Flat "); ImGui::SameLine();
sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f);
code_block(
"float history[] = {10,12,11,15,18,17,20};\n"
"sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);"
);
}
} // namespace gallery
-230
View File
@@ -1,230 +0,0 @@
// primitives_gallery — catalogo visual interactivo de los primitivos UI
// del registry (cpp/functions/core y cpp/functions/viz).
//
// Sidebar izquierdo con lista de primitivos agrupados por dominio; panel
// derecho renderiza la demo del item seleccionado (+ snippet de codigo).
//
// Rol: smoke test visual + documentacion viva + build gate en CI.
// NO se conecta a sqlite_api ni a ningun backend. Datos sinteticos.
#include "app_base.h"
#include "imgui.h"
#include "core/fullscreen_window.h"
#include "core/tokens.h"
#include "core/page_header.h"
#include "core/toast.h"
#include "core/app_menubar.h"
#include "core/tree_view.h"
#include "demos.h"
#include "demo.h"
#include "capture.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <vector>
struct DemoEntry {
const char* id; // id estable, apto para comparar seleccion
const char* label; // texto en sidebar
const char* category; // "Core" o "Viz"
void (*fn)(); // puntero a la demo_xxx
};
static const DemoEntry k_demos[] = {
// Core
{"button", "button", "Core", &gallery::demo_button},
{"icon_button", "icon_button", "Core", &gallery::demo_icon_button},
{"toolbar", "toolbar", "Core", &gallery::demo_toolbar},
{"modal_dialog", "modal_dialog", "Core", &gallery::demo_modal},
{"text_input", "text_input", "Core", &gallery::demo_text_input},
{"select", "select", "Core", &gallery::demo_select},
{"toast", "toast + inbox", "Core", &gallery::demo_toast},
{"tree_view", "tree_view", "Core", &gallery::demo_tree_view},
{"badge", "badge", "Core", &gallery::demo_badge},
{"empty_state", "empty_state", "Core", &gallery::demo_empty_state},
{"page_header", "page_header", "Core", &gallery::demo_page_header},
{"dashboard_panel", "dashboard_panel", "Core", &gallery::demo_dashboard_panel},
{"kpi_card", "kpi_card", "Core", &gallery::demo_kpi_card},
{"text_editor", "text_editor", "Core", &gallery::demo_text_editor}, // wave 1
{"file_watcher", "file_watcher", "Core", &gallery::demo_file_watcher}, // wave 1
{"process_runner", "process_runner", "Core", &gallery::demo_process_runner},
{"tween", "tween_curves", "Core", &gallery::demo_tween},
{"bezier_editor", "bezier_editor", "Core", &gallery::demo_bezier_editor},
{"timeline", "timeline", "Core", &gallery::demo_timeline},
{"sql_workbench", "sql_workbench", "Core", &gallery::demo_sql_workbench}, // issue 0032
// Viz
{"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart},
{"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart},
{"line_plot", "line_plot", "Viz", &gallery::demo_line_plot},
{"scatter_plot", "scatter_plot", "Viz", &gallery::demo_scatter_plot},
{"histogram", "histogram", "Viz", &gallery::demo_histogram},
{"sparkline", "sparkline", "Viz", &gallery::demo_sparkline},
{"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph},
{"graph_styles", "graph_styles", "Viz", &gallery::demo_graph_styles}, // issue 0049f
{"candlestick", "candlestick", "Viz", &gallery::demo_candlestick},
{"gauge", "gauge", "Viz", &gallery::demo_gauge},
{"heatmap", "heatmap", "Viz", &gallery::demo_heatmap},
{"table_view", "table_view", "Viz", &gallery::demo_table_view},
{"surface_plot_3d", "surface_plot_3d", "Viz", &gallery::demo_surface_plot_3d},
{"scatter_3d", "scatter_3d", "Viz", &gallery::demo_scatter_3d},
{"mesh_viewer", "mesh_viewer", "Viz", &gallery::demo_mesh_viewer},
{"treemap", "treemap", "Viz", &gallery::demo_treemap},
{"sankey", "sankey", "Viz", &gallery::demo_sankey},
{"chord", "chord", "Viz", &gallery::demo_chord},
{"contour", "contour", "Viz", &gallery::demo_contour},
{"voronoi", "voronoi", "Viz", &gallery::demo_voronoi},
// Gfx (shaders_lab core)
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
{"gl_info", "gl_info", "Gfx", &gallery::demo_gl_info}, // issue 0049b
};
static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]);
static std::string g_selected_id = "button";
static const DemoEntry* find_demo(const std::string& id) {
for (int i = 0; i < k_demo_count; i++) {
if (id == k_demos[i].id) return &k_demos[i];
}
return &k_demos[0];
}
static void draw_sidebar() {
ImGui::BeginChild("##gallery_sidebar", ImVec2(220, 0),
ImGuiChildFlags_Borders);
// Agrupar por categoria como rama del tree_view (categorias abiertas por
// defecto). Cada demo es una hoja seleccionable.
int i = 0;
while (i < k_demo_count) {
const char* category = k_demos[i].category;
// Default-open la rama la primera vez que se abre el sidebar.
ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
if (fn_ui::tree_branch_begin(category, category, /*selected=*/false)) {
// Recorrer todas las demos consecutivas con esta misma categoria.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
const auto& d = k_demos[i];
const bool selected = (g_selected_id == d.id);
fn_ui::tree_leaf(d.id, d.label, selected);
if (fn_ui::tree_node_clicked()) {
g_selected_id = d.id;
}
i++;
}
fn_ui::tree_branch_end();
} else {
// Rama colapsada — saltar todos sus items.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
i++;
}
}
}
ImGui::EndChild();
}
static void render() {
// Theme y gl_loader gestionados por fn::run_app (theme=FnDark por defecto,
// init_gl_loader=true en AppConfig). Menubar via run_app.
// auto_dockspace=false porque usamos fullscreen_window que ocupa todo.
fullscreen_window_begin("##gallery");
page_header_begin("Primitives Gallery",
"Visual catalog of fn_registry C++ UI primitives");
page_header_end();
if (ImGui::BeginTable("##layout", 2,
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("sidebar", ImGuiTableColumnFlags_WidthFixed, 220.0f);
ImGui::TableSetupColumn("content", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
draw_sidebar();
ImGui::TableSetColumnIndex(1);
ImGui::BeginChild("##gallery_content", ImVec2(0, 0),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_HorizontalScrollbar);
// Cuando cambia el tamaño de fuente (Settings > Size), el contenido
// del child crece/encoge pero la posicion de scroll en pixeles
// no — efecto: lo visible "se baja". Escalamos scroll_y por el
// ratio de fuentes para mantener la misma linea logica arriba.
{
static float s_prev_font_size = 0.0f;
float cur_font_size = ImGui::GetStyle().FontSizeBase;
if (s_prev_font_size > 0.0f &&
std::fabs(s_prev_font_size - cur_font_size) > 0.01f) {
ImGui::SetScrollY(ImGui::GetScrollY() *
(cur_font_size / s_prev_font_size));
}
s_prev_font_size = cur_font_size;
}
const DemoEntry* d = find_demo(g_selected_id);
if (d && d->fn) d->fn();
ImGui::EndChild();
ImGui::EndTable();
}
fullscreen_window_end();
// Toasts se renderizan encima para que el demo de toast funcione aqui tambien.
fn_ui::toast_render();
}
int main(int argc, char** argv) {
// Capture mode: `primitives_gallery --capture <output_dir>` corre cada
// demo en una ventana GLFW invisible y guarda PNG por demo. Para CI/golden.
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--capture") == 0) {
if (i + 1 >= argc) {
std::fprintf(stderr, "--capture requires an output dir argument\n");
return 2;
}
const char* out_dir = argv[i + 1];
// Best-effort mkdir (idempotente). Windows mkdir() solo acepta el path.
#if defined(_WIN32)
mkdir(out_dir);
#else
mkdir(out_dir, 0755);
#endif
std::vector<gallery::CaptureItem> items;
items.reserve(k_demo_count);
for (int j = 0; j < k_demo_count; j++) {
items.push_back({k_demos[j].id, k_demos[j].fn});
}
gallery::CaptureConfig cfg;
cfg.output_dir = out_dir;
cfg.warmup_frames = 3;
cfg.capture_w = 800;
cfg.capture_h = 600;
const bool ok = gallery::run_capture(cfg, items);
return ok ? 0 : 1;
}
}
return fn::run_app(
{.title = "fn_registry · Primitives Gallery",
.width = 1400,
.height = 900,
.viewports = true,
.about = {.name = "Primitives Gallery",
.version = "0.4.0",
.description = "Visual catalog of fn_registry C++ UI primitives. Now on OpenGL 4.3 core (compute, SSBOs, image load/store) — ver demo gl_info."},
.init_gl_loader = true,
.auto_dockspace = false,
.log = {"primitives_gallery.log", 1}},
render
);
}
@@ -1,47 +0,0 @@
# Tables playground (cpp_apps.md / playgrounds.md). NO se indexa.
# Build flag FN_TQL_DUCKDB=ON activa el adapter tql_duckdb (issue 0080).
option(FN_TQL_DUCKDB "Enable DuckDB SQL execution adapter for tables playground" OFF)
set(_TABLES_SRC
main.cpp
data_table.cpp
data_table_logic.cpp
llm_anthropic.cpp
lua_engine.cpp
tql.cpp
tql_to_sql.cpp
viz.cpp
)
set(_TABLES_TEST_SRC
self_test.cpp
data_table_logic.cpp
llm_anthropic.cpp
lua_engine.cpp
tql.cpp
tql_to_sql.cpp
)
if(FN_TQL_DUCKDB)
list(APPEND _TABLES_SRC tql_duckdb.cpp)
list(APPEND _TABLES_TEST_SRC tql_duckdb.cpp)
endif()
add_imgui_app(tables_playground ${_TABLES_SRC})
target_link_libraries(tables_playground PRIVATE lua54 implot)
if(FN_TQL_DUCKDB)
target_compile_definitions(tables_playground PRIVATE FN_TQL_DUCKDB=1)
target_link_libraries(tables_playground PRIVATE duckdb_vendored)
duckdb_copy_runtime(tables_playground)
endif()
# Self-test E2E (logica pura + lua_engine + tql).
add_executable(tables_playground_self_test ${_TABLES_TEST_SRC})
target_include_directories(tables_playground_self_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/functions
)
target_link_libraries(tables_playground_self_test PRIVATE lua54)
if(FN_TQL_DUCKDB)
target_compile_definitions(tables_playground_self_test PRIVATE FN_TQL_DUCKDB=1)
target_link_libraries(tables_playground_self_test PRIVATE duckdb_vendored)
duckdb_copy_runtime(tables_playground_self_test)
endif()
File diff suppressed because it is too large Load Diff

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