636 Commits

Author SHA1 Message Date
egutierrez ea6a3ec8a5 chore: auto-commit (3 archivos)
- docs/adr/README.md
- docs/adr/0005-keep-parent-git-lean.md
- docs/diary/2026-06-03.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 12:50:09 +02:00
Egutierrez 3c1061fbd8 feat(core): dag_parse parsea continue_on.exit_code + retry_policy (v1.1.0)
DagContinueOn gana el campo ExitCodes []int (codigos de salida tolerados) y el
parser mapea continue_on.exit_code desde el YAML. retry_policy (limit,
interval_sec) ya existia en el modelo y ahora queda documentado como contrato
estable para los executors.

Funcion pura: solo normaliza el YAML al modelo DagDefinition; la interpretacion
(reintentar, tolerar codigos) vive en el executor que lo consuma (dag_engine).

Test: 'parsea continue_on.exit_code y retry_policy'. Tag de grupo: scheduler.
2026-06-03 12:44:26 +02:00
egutierrez ba5d262c6c chore: submodulos C++ en modo shallow (depth 1)
Marca todos los submodulos de cpp/vendor (y emsdk) como shallow en .gitmodules
para que los clones futuros descarguen solo el commit pinneado en vez de toda la
historia upstream. Reduce .git/modules de ~338MB a ~35MB sin perder codigo: el
working tree mantiene el snapshot completo de cada dependencia. Reversible por
submodulo con 'git fetch --unshallow'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:46:11 +02:00
egutierrez c5fb6cab76 feat(core): describe_cron_expr_go_core + ignorar build/ raiz
Funcion pura que describe expresiones cron en lenguaje natural estilo
crontab.guru (consumida por el tooltip de schedule del frontend de dag_engine
via /api/cron/describe). Tambien se anade build/ (raiz) al .gitignore para que
los artefactos de compilacion C++ no se vuelvan a versionar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:34:50 +02:00
egutierrez 4bce095964 refactor(infra): split de drivers pesados a subpaquetes + fix TestSSEHandler
Mueve duckdb_open, clickhouse_open, postgres_open, matrix_* y keyring_token_store
del paquete monolitico functions/infra a subpaquetes propios
(functions/infra/{duckdb,clickhouse,postgres,matrix,keyring}). El paquete infra ya
no importa los drivers (go-duckdb, clickhouse-go, pgx, mautrix, go-keyring), por lo
que las apps que solo usan funciones ligeras (process, cron, http, sqlite) dejan de
arrastrarlos. Reduccion de binarios: dag_engine 72->10MB, registry_api 70->8.7MB,
services_api 70->9MB, call_monitor 68->6.6MB, sqlite_api 70->8.9MB.

Los IDs del registry se mantienen estables (domain: infra en frontmatter). Se
preservan los build tags goolm/libolm de matrix_crypto_init.

Tambien corrige TestSSEHandler: el test leia el body con un unico Read() que con
HTTP chunked solo capturaba el primer evento; ahora usa io.ReadAll hasta EOF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 23:48:59 +02:00
egutierrez 4ad4e7093a chore(hooks): activa monitorizacion del registry en settings.local.json
Reconstruye el cableado de hooks del bucle reactivo (issues 0085-0087), que no
viajaba entre PCs por no estar versionado:
- PostToolUse: hook_call_monitor (capturador de calls/code_writes/violations)
              + hook_capability_tag_gate.
- PreToolUse: hook_registry_mcp (aviso sqlite3 directo) + hook_fn_match.
- UserPromptSubmit: hook_capabilities_inject + hook_registry_first_reminder.
Rutas via $CLAUDE_PROJECT_DIR para portabilidad entre maquinas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 23:00:10 +02:00
egutierrez c92ecf74ce fix(cpp): include <cstdint>/<cstddef> en data_table_types.h para portabilidad Linux
Los headers usaban uint8_t/uint32_t en enum-base-types (ColorRuleKind,
CellRenderer, TableEventKind) y en StringPool sin incluir <cstdint>/<cstddef>.
MSVC los arrastra transitivamente; GCC no, lo que rompia la compilacion del
modulo data_table y de toda app que lo enlaza (registry_dashboard, etc.) en
Linux nativo. Una sola cabecera afectada; el resto los obtiene transitivamente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 23:00:10 +02:00
egutierrez e18528a677 feat(cybersecurity): auto-commit con 2 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 22:25:59 +02:00
egutierrez 107585697e feat(browser): auto-commit con 3 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 22:12:43 +02:00
egutierrez f74d0d8e32 feat(cybersecurity): auto-commit con 10 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 21:50:23 +02:00
egutierrez 4bbfc127ad fix(infra): auto-recover de push rechazado + limpieza de gitlinks fantasma
full_git_push: el auto-recover de non-fast-forward nunca se disparaba por dos
bugs. (1) git_push_if_ahead hacia `tail -1` del error de push y se quedaba con
la linea final de `hint:`, perdiendo "[rejected]" y "Updates were rejected", de
modo que el pipeline no detectaba el rechazo. Ahora se preservan las lineas con
los keywords (rejected/fast-forward/fetch first). (2) El paso 4 capturaba stdout
y stderr juntos (2>&1), metiendo lineas "[push]" de stderr al principio del
status y rompiendo el glob `== "[error]"*` anclado al inicio; ahora solo captura
stdout y la deteccion cubre todas las redacciones de git. (3) El merge auto
evaluaba el exit de `tail` (siempre 0) en vez del de `git merge`; un merge con
conflictos se reportaba como exito. Ahora se evalua el exit real del merge y se
aborta limpio ante conflictos.

Limpieza: cpp/apps/chart_demo y cpp/apps/shaders_lab estaban commiteados como
gitlinks (submodulos, mode 160000) sin URL en .gitmodules, directorios vacios
fantasma que rompian el paso 3 de full_git_pull con "fatal: No url found for
submodule path". Eliminados del indice. Las apps C++ reales viven en apps/ como
sub-repos Gitea independientes (regla cpp_apps.md; cpp/apps/ deprecado por
issue 0096).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 22:31:57 +02:00
egutierrez ee86dd6d11 Merge remote-tracking branch 'origin/master' 2026-06-01 22:23:26 +02:00
egutierrez 7913116a8e chore: auto-commit (129 archivos)
- .claude/agents/fn-analizador/SKILL.md
- .claude/agents/fn-constructor/SKILL.md
- .claude/agents/fn-executor/SKILL.md
- .claude/agents/fn-mejorador/SKILL.md
- .claude/agents/fn-orquestador/SKILL.md
- .claude/agents/fn-recopilador/SKILL.md
- .claude/commands/app.md
- .claude/commands/compile.md
- .claude/commands/cpp-app.md
- .claude/commands/create_functions.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 22:23:12 +02:00
egutierrez ec9857acbe chore: auto-commit (7 archivos)
- python/functions/core/__init__.py
- python/functions/pipelines/metabase_bulk_add_users_to_group.md
- python/functions/pipelines/metabase_bulk_add_users_to_group.py
- cpp/apps/
- python/functions/core/clean_email.md
- python/functions/core/clean_email.py
- python/functions/core/clean_email_test.py

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 11:39:08 +02:00
egutierrez dbf5b45acd chore: auto-commit (7 archivos)
- .mcp.json
- bash/functions/pipelines/full_git_push.sh
- python/pyproject.toml
- python/uv.lock
- bash/functions/infra/jupyter_mcp_serve.md
- bash/functions/infra/jupyter_mcp_serve.sh
- dev/issues/0166-app-to-app-dependencies-tracking.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 01:29:45 +02:00
egutierrez dfcb3c46a7 fix(infra): full_git_push auto-init handles .git without origin
Auto-init step skipped any dir with .git present, even when it lacked
an origin remote. Such dirs fell through to push step and failed with
"'origin' does not appear to be a git repository". Now skip only when
.git AND origin exist; otherwise run ensure_repo_synced to create the
Gitea repo + add origin + push.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 17:34:21 +02:00
egutierrez fd5787c55f chore: auto-commit (43 archivos)
- .mcp.json
- bash/functions/infra/write_mcp_jupyter_config.md
- bash/functions/infra/write_mcp_jupyter_config.sh
- cpp/CMakeLists.txt
- cpp/apps/chart_demo
- cpp/apps/shaders_lab
- cpp/functions/gfx/gl_framebuffer.cpp
- cpp/functions/gfx/gl_framebuffer.h
- cpp/functions/gfx/gl_framebuffer.md
- cpp/functions/gfx/mesh_gpu.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 17:28:47 +02:00
egutierrez a2efdcf003 merge: origin/master into local 2026-05-27 18:48:28 +02:00
egutierrez e5fac0c71f chore: auto-commit (2 archivos)
- dev/issues/completed/0126-pipeline-launcher-migration-003.md
- dev/proposals_e2e_checks_0121/pipeline_launcher.yaml

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:48:14 +02:00
egutierrez 5a9adb0896 issue(0128): kanban file attachments — PR draft en dataforge/kanban#1 2026-05-27 10:53:13 +02:00
egutierrez 621e8895c9 feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:15 +02:00
egutierrez 34c27876e0 feat(compile): Wails support in /compile skill
- New helper: deploy_wails_exe_to_windows_bash_infra
  - taskkill + cp build/bin/<app>.exe to Desktop/apps/<app>/
  - cmd.exe /c start RELAUNCHES the app post-deploy (key diff vs cpp)
  - preserves local_files/, copies appicon.ico if present
- New pipeline: compile_wails_app_bash_pipelines
  - resolve_cpp_app_dir (reused) + wails build -platform windows/amd64
  - auto -tags goolm if app declares matrix_crypto_init
  - delegates deploy + relaunch to deploy_wails_exe_to_windows
- /compile skill dispatches by framework:
  - wails.json present -> compile_wails_app (relaunches)
  - CMakeLists.txt present -> compile_cpp_app (no relaunch)

Refs: matrix_client_pc + matrix_admin_panel (issues 0147, 0163)
2026-05-25 03:11:09 +02:00
egutierrez 22544fbb08 chore(issues): close 0166 livekit TURN deploy
Integrated LiveKit TURN deployed on organic-machine.com:
- UDP 3478 + TCP 5349 (not 443 — Traefik HTTP/3 owns it)
- Wildcard cert *.organic-machine.com extracted from Traefik acme.json
- Subdomain turn-matrix-rtc-320bd4.organic-machine.com (wildcard DNS+cert)
- VPS commit f7f5303 in egutierrez/element_matrix_chat

DoD acceptance items requiring real-world CGNAT call testing
deferred to operator (no agent way to test mobile 4G NAT).
2026-05-25 00:46:43 +02:00
egutierrez 00c7fc728e chore(issues): close 0167+0168+0169+0170 livekit hardening bundle
VPS commit: 8eef89b (egutierrez/element_matrix_chat)

- 0167: STUN leak fixed (use_external_ip:false + node_ip hardcoded)
- 0168: UDP range expanded 50000-50200 -> 50000-50500
- 0169: API secret rotated (old key LK44e009c6e92b -> new LK5f6b38bb)
- 0170: livekit.example.yaml refreshed + header comments cleaned

Verification:
- 0 STUN packets to Google during restart (tcpdump 60s window)
- Endpoint /livekit/sfu/ HTTP 200
- LiveKit logs: nodeIP=135.125.201.30, portICERange=[50000,50500]
- Containers livekit + livekit-jwt healthy

New secret stored in pass: matrix/livekit-secret-rotation-2026-05-25
2026-05-25 00:44:15 +02:00
egutierrez 1cbbad9ff9 feat(matrix): 4 synapse quick wins applied + 6 follow-up issues
Server-side homeserver.yaml on organic-machine VPS:
- encryption_enabled_by_default_for_room_type: invite -> all
- presence.enabled: false (block EDU metadata leak)
- url_preview_enabled: false (block SSRF + IP leak)
- msc4108 rendezvous endpoint uncommented (QR login)

Synapse restarted, /versions shows e2ee_forced.* + msc4108 unstable
features active. Backup at synapse_data/homeserver.yaml.bak.1779659423.

Issues opened for remaining gaps:
- 0165 LUKS for media_store (at-rest encryption)
- 0166 LiveKit TURN deploy (NAT traversal gap)
- 0167 STUN leak to Google (hardcode external_ip)
- 0168 UDP range expand 200 -> 500
- 0169 LIVEKIT_SECRET rotation (audit exposure)
- 0170 livekit.example.yaml rename hygiene

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:53:37 +02:00
egutierrez c441366f89 feat(matrix-mas): 3 helpers for matrix_client_pc (issue 0147)
- mas_oidc_loopback_go_infra: OAuth2 PKCE + loopback HTTP for desktop login
- keyring_token_store_go_infra: persist OAuth tokens in SO keychain
- matrix_client_init_go_infra: init mautrix.Client from access_token + whoami

Plus go.work workspace including matrix_client_pc sub-repo for shared
import path during dev. All 3 fns tagged matrix-mas capability group.

Tests: TestMasOidcLoopback (15 cases), TestKeyringTokenStore (5 cases,
SKIP on headless), TestMatrixClientInit (6 cases) — all green/skip.

Refs: dev/issues/0147-matrix-client-pc-scaffold.md
Refs: dataforge/matrix_client_pc commit f28c2b1
2026-05-24 23:23:49 +02:00
egutierrez daef7ea190 feat(matrix): MAS migration helpers + 2 flows + 15 issues + capability group
Helper functions (matrix-mas capability group):
- mas_client_register_bash_infra: register/sync OAuth clients via mas-cli
- mas_syn2mas_migration_bash_infra: dry-run + apply user migration to MAS
- synapse_msc3861_enable_go_infra: edit homeserver.yaml MSC3861 block (with diff)
- wellknown_oidc_patch_go_infra: patch well-known JSON with msc2965.authentication
- synapse_login_flows_check_go_infra: health-check post-migration login flows

Flows + issues for custom Matrix clients (PC + Android):
- 0010 matrix-client-pc: Wails + React+Mantine (issues 0147-0153)
- 0011 matrix-client-android: Kotlin + Compose (issues 0154-0161)
- 0162 enable MAS as auth provider (Synapse delegate) — EXECUTED on VPS
- 0163 custom admin panel propio (sustituye synapse-admin)

Production state (organic-machine.com):
- Synapse migrated SQLite -> Postgres
- MSC3861 active, password_config disabled
- 21 users + 41 access_tokens migrated via syn2mas
- 4 MAS clients registered (element, matrix_pc, matrix_android, admin_panel)
- synapse-admin container removed + Coolify route deleted
- well-known patched with org.matrix.msc2965.authentication

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:53:33 +02:00
egutierrez 3a8b4c2179 merge(0133): columnar snapshot + string pool + reader rewire (1+2+3)
Foundation (ce7470d5) + reader rewire (2f7fdd40).

- ColumnSnapshot per col (i64/f64/str_ids) + StringPool per-State
- compute_visible_rows filter/sort uses snapshot direct numeric/id compare
- StringPool realloc-crash fix (reserve before emplace_back)
- Pool staleness sentinel (rebuild when string_pool.size() drift)
- High-cardinality cap (>2048 unique → skip interning, fallback raw)

API publica intacta. Bench 100k sort_numeric +131% vs baseline.
text_editor_smoke RED preexisting unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:22:38 +02:00
egutierrez 2f7fdd407b feat(0133-3): wire filter/sort readers to columnar snapshot
Change 3 of issue 0133 — rewire compute_visible_rows, filter eval,
and sort comparators to read from the SnapshotCache when available.

Hot paths rewired:
- compute_visible_rows (overload with snap): filter eval uses
  compare_snap (fast i64/f64 numeric compare for Int/Float cols;
  id-compare for low-cardinality string Eq/Neq; raw cells fallback
  for Contains/StartsWith/EndsWith).
- Sort comparators: direct i64/f64 array compare for Int/Float cols
  (goto sort_done skips string fallback); string sort uses uint32_t
  id compare with pool lookup only on mismatch.
- Stage>0 filter/sort: same snapshot overload.

Materialization paths (build_so, s0_backing, mat_backing, config popup)
kept on raw cells — they copy into std::string anyway, no benefit from
snapshot and snprintf-per-cell was 2M extra calls per frame.

Bug fixes (required for correctness):
1. StringPool::intern() realloc safety: force reserve before
   emplace_back so string_view keys in the map never go dangling.
2. SnapshotCache::pool_size_built sentinel: detects when a new State
   is created with an empty pool but same cells pointer (begin_scenario
   pattern). Prevents str_ids from indexing into an empty pool (SIGSEGV).
3. Cardinality cap (2048 uniques / 25% sample): high-cardinality string
   cols (timestamps-as-strings, UUIDs, names) skip interning — str_ids
   stays empty and compare_snap falls back to raw cells. Prevents 30MB+
   pool bloat that hurt cache for filter/sort on other cols.

Bench delta vs baseline (100k rows, LIBGL_ALWAYS_SOFTWARE=1):
  linear_scroll: 16.0 -> 15.5 fps p50  (-3%, baseline already FAIL)
  filter_like:   59.7 -> 56.0 fps p50  (-6%, still PASS at 56fps)
  sort_numeric:   3.9 ->  9.0 fps p50 (+131%, snapshot i64 sort)
  color_rule:    15.2 -> 14.8 fps p50  (-3%, baseline already FAIL)

Build: green for all 10 available Linux consumers (text_editor_smoke
linker failure is preexisting, not caused by this change).

API public intact. TableEvent.row indexing TableInput preserved.
Pointer-identity invalidation preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 00:21:09 +02:00
egutierrez fe0265c3bf docs(0133): MIGRATION.md + growth log placeholder + drift fix
- modules/data_table/MIGRATION.md: porting guide + release checklist 1.0.0-stable
- data_table.md: growth log entry commented for post-gate bump
- data_table.md: fix error_type Go remnant ("error_go_core" -> "") in C++ module
- cpp/CMakeLists.txt: SQLite3 optional dep for data_table_bench (cross-windows)
- agent_cleanup_worktree.go: !windows build tag (uses unix-only syscalls)
- dev/issues/0133-cpp-data-table-10m-rows.md: issue tracking

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:56:14 +02:00
egutierrez 6a318bf0c9 fix(0132): terminal_panel black bg + prompt input + cross-platform demos + e2e
terminal_panel.cpp:
  - BeginChild con PushStyleColor(ChildBg, negro) + PushStyleColor(Text, gris claro)
  - PushStyleVar(WindowPadding, 8/6px) para padding terminal real
  - Input prompt siempre visible cuando readonly=false
  - Prefijo "$ " antes del InputText (TextUnformatted + SameLine)
  - BeginDisabled() cuando el shell esta cerrado (en vez de ocultar el widget)
  - Calculo de child_h reserva exactamente GetFrameHeightWithSpacing+6 para el prompt

cpp/tests/e2e/test_terminal_panel_e2e.py (nuevo):
  - 4 asserts: PNG existe, no todo-blanco, region oscura >= 30%, pixels no-negros >= 0.3%
  - Lanza primitives_gallery --capture, busca el binario Linux o Windows.exe automaticamente
  - Skip graceful si no hay GL ni binario (WSL/CI headless)
  - 4/4 pasan en Linux con LIBGL_ALWAYS_SOFTWARE=1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:48:42 +02:00
egutierrez c8d6ba5c13 feat(0133): register data_table_bench in cpp/CMakeLists.txt
Adds the add_subdirectory block for apps/data_table_bench so the build
system picks it up. The app itself lives in its own sub-repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:38:17 +02:00
egutierrez ce7470d5f5 feat(0133-1+2): columnar snapshot + string interning in data_table
Change 1 — Columnar Snapshot Internal:
- Add ColumnSnapshot struct (type + str_ids/i64/f64 per column) in data_table_internal.h
- Add SnapshotCache struct with pointer-identity sentinel (last_cells_ptr)
- Add SnapshotCache field to UiState singleton
- In render(): rebuild snapshot after join materialization when cells ptr changes
  Uses same pointer-identity pattern as existing stats_last_cells in State
  Int/Float columns parsed once via parse_number; String/Auto interned

Change 2 — String Interning:
- Add StringPool struct (strings + unordered_map<string_view, uint32_t>) to data_table_types.h
- StringPool is per-State (NOT global) for table isolation
- intern(sv) inserts if absent, returns stable uint32_t index
- Cleared + rebuilt on each snapshot rebuild for index coherence
- Add string_pool field to State struct

Documentation:
- Extended header comment in data_table_internal.h describing design,
  StringPool API, invariants (pointer-identity, row→snapshot_row),
  and how stats_last_cells and snapshot coexist independently

Build: fn_module_data_table + tables_qa pass, no new errors (only
pre-existing -Wformat-truncation warnings unrelated to this change).
Public API (data_table.h, TableInput, render() signature) unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:35:12 +02:00
egutierrez 07252c0172 feat(0132): cpp terminal_panel module + ansi_parser
Nuevo modulo reutilizable terminal_panel (fn_term) para ImGui:

Sub-fn ansi_parser_cpp_core (cpp/functions/core/):
- Parser ANSI/VT100 byte-a-byte sin heap allocs por evento
- SGR colores FG/BG 16-color + bold + reset
- Cursor moves CUU/CUD/CUF/CUB + CUP absoluto
- Erase ED(2)/EL(2), CR/LF/BS
- Statemachine 4 estados, thread-unsafe por diseno
- 21 tests unitarios (57 assertions), todos pasan

terminal_panel_cpp_viz (cpp/functions/viz/terminal_panel/):
- terminal_panel.cpp: render ImGui + process_output con list clipper
- terminal_panel_linux.cpp: forkpty + reader thread no-blocking
- terminal_panel_windows.cpp: ConPTY CreatePseudoConsole (SDK >= 17763)
- Scrollback circular configurable (default 5000 lineas)
- Toolbar: clear, copy, reset, scroll-lock + status indicator
- readonly mode: sin input box, send() es no-op
- uses_functions: ansi_parser_cpp_core, logger_cpp_core

Tests:
- test_ansi_parser.cpp: 21 test cases, 57 assertions (PASS)
- test_terminal_panel_smoke.cpp: 3 test cases (PASS: spawn echo hello,
  process exits cleanly, readonly ignores send)

CMake:
- cpp/tests/CMakeLists.txt: add test_ansi_parser + test_terminal_panel_smoke
- primitives_gallery (sub-repo): ver commit separado en apps/primitives_gallery

Pendiente (anti-scope v1):
- Windows ConPTY: stub funcional que compila; join() del reader thread
  via std::thread no implementado (usa CreateThread detached)
- ANSI 256/24-bit color, italics, Unicode wide
- Curses pesados (vim, htop, top) — cursor visible basic solo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:35:11 +02:00
egutierrez 3791afa216 done(0131): agents v0.2 — unified control + uptime/msg_24h + data_table + clear/cache 2026-05-22 23:10:32 +02:00
egutierrez 67e0abf295 feat(issues): 0131 agents v0.2 — unified control + uptime/msg_24h + data_table + clear/cache 2026-05-22 22:47:02 +02:00
Egutierrez c468b24d2b feat(0130): kanban_cpp v2 — backend Go + 5 registry parser fns + epic/sub-issues
Registry (issue 0130a):
- 5 fns infra: parse_issue_md, write_issue_md, scan_issues_dir,
  scan_flows_dir, watch_dir_fsnotify
- 3 tipos: Issue, Flow, FsEvent
- Tests round-trip + scan reales + watcher fsnotify (all PASS)
- Capability group 'kanban' nuevo (docs/capabilities/kanban.md)

Apps:
- apps/kanban_cpp/ (sub-repo) — frontend ImGui: board drag-drop,
  flows, filters, detail con CSV editors
- apps/kanban_cpp/backend/ — Go service port 8487: REST + SSE +
  fsnotify watcher, parser bidireccional MD<->SQLite cache

Issues:
- dev/issues/0130-kanban-cpp-v2.md (epic)
- 0130a parser, 0130b backend, 0130c frontend

CMakeLists.txt: add_subdirectory apps/kanban_cpp (registrado por
init_cpp_app scaffolder).

End-to-end verde: backend devuelve 189 issues + 9 flows; PATCH a
/api/issues/{id} reescribe .md (solo frontmatter, body intacto);
frontend --self-test exit 0; tests Go infra 5/5 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:20:15 +02:00
egutierrez e387c91b4c fix(http_request): drop "2>&1" on Windows — CreateProcessW has no shell
POSIX popen routes via /bin/sh -c, so "2>&1" is a shell redirect. On
Windows we use CreateProcessW directly (no shell): curl receives "2>&1"
as a positional arg, treats it as a second URL, and fails with exit 3
"URL rejected: Bad hostname".

Stderr is already merged into the same pipe via STARTUPINFOW.hStdError
on Windows, so the redirect is also unnecessary there. Guard with
#ifndef _WIN32.

Also adds FN_HTTP_DEBUG env var to dump the cmdline + req.url for
future bug triage (zero-cost when unset).

Detected via agents_dashboard.exe --connect-test against
https://agents.organic-machine.com — same .exe with the fix now returns
"OK 11" in <2s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:15:37 +02:00
egutierrez cc1e88fe55 done: 0128 + 0129 — agents_and_robots HTTP API + agents_dashboard C++ ImGui
Both issues delivered end-to-end:

0128 (backend, merged via dataforge/agents_and_robots/pulls/1):
- HTTP daemon in cmd/launcher with apikey Bearer auth + SSE
- LIVE at https://agents.organic-machine.com via Coolify Traefik + LE cert
- systemd Restart=always
- Unified status autodetect fix applied

0129 (frontend, merged via dataforge/agents_dashboard/pulls/1):
- C++ ImGui app in projects/element_agents/apps/agents_dashboard
- 4 panels: Connection / Agents / Logs / Status
- secret_store_cpp_infra new function (DPAPI Windows / XOR Linux)
- Deployed to Windows Desktop, App Hub tarjeta visible

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:58:05 +02:00
egutierrez 221fe6dab6 merge: 0129 agents_dashboard scaffold + secret_store_cpp_infra + CMakeLists register 2026-05-22 21:57:04 +02:00
egutierrez ca0e6ac584 chore: auto-commit (3 archivos)
- cpp/functions/infra/secret_store.cpp
- cpp/functions/infra/secret_store.h
- cpp/functions/infra/secret_store.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:52:37 +02:00
egutierrez 25e1b30a3f 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 61507ee502 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 70106c6195 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 32cb2ac0f1 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 50a0875c6b 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 dce725e69f 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 68ec1bd70a 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 1c8bc19f38 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 eb29dc54c3 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 d8483176a4 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 7b0b697b18 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 32bb2cbd25 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 bed8a802a0 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 980f8807a9 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 68bb9fbdae 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 b8f8b8ff3c 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 53a3cdbda9 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 2b5c30a022 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 78450b7039 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 ef67cd2d15 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 bac5ad2b45 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 93520c4319 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 a192ee0695 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 eb57e83392 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 741c86c042 merge issue/0119: kanban_cpp issues/flows sync layer 2026-05-18 18:57:11 +02:00
agent a218092034 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 89a6472e4e merge issue/0116: skill_tree v2 Launch workflow boton + feature flag legacy_claude_fix 2026-05-18 18:47:36 +02:00
egutierrez 9c90c28c24 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 051fbb9be5 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 981e22d21a 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 3f3e21cb35 merge issue/0118: agent_runs_timeline C++ ImGui + helpers
# Conflicts:
#	cpp/tests/CMakeLists.txt
2026-05-18 18:33:02 +02:00
egutierrez b9e85485cb merge issue/0117: dod_evidence_panel C++ ImGui + helpers 2026-05-18 18:32:35 +02:00
egutierrez ab884dba0b merge issue/0115: agent_launch_worktree + agent_cleanup_worktree Go fns + capability group agents 2026-05-18 18:32:31 +02:00
egutierrez 9440470a73 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 4ab3678b45 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 af26c78e70 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 048a4a1457 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 b341d5d9ad 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 a498a20ddf 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 cca3adb65d 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 a4077913ef 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 c1e88af5c7 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 e271b6e7f8 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 a4d1e5272a docs: cerrar issue 0115 (worktree launcher Go fn) 2026-05-18 18:24:24 +02:00
egutierrez 3efc7ede29 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 1b6d7940da 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 a87c992cd9 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 73834ff842 merge issue/0113: agent_runner_api service Go :8486 + agent_runs.db 2026-05-18 18:18:14 +02:00
egutierrez 07503b8fff 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 7ee351a505 merge issue/0114: DoD evidence schema + fn doctor dod 2026-05-18 18:17:20 +02:00
egutierrez b9716a7cd6 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 0e1b3e3aa0 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 1f73c18b8f 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 89be293d2f 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 975c9f66ae docs: cerrar issue 0113 — agent_runner_api scaffold + DoD + worktrees 2026-05-18 18:14:39 +02:00
egutierrez 1cdbe1bcd1 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 df1eb701d3 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 581d0f0a0e 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 c5587842b9 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 78d955fd72 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 ddb5366884 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 fad4006f60 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 6ad82167bb 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 a03675113a 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 0b9af8f1bb 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 4acf6986d3 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 304f48ccac 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 3aa0f2b003 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 407dd167f0 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 7b95c01320 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 97bd5ea056 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 1c9aabd212 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 8aa429b313 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 d0eb41f3cd 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 eba69e4573 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 7e2fb05144 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 110791ff45 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 acecbbc821 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
egutierrez fad7b2fccc feat(pipelines): auto-commit con 3 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:13:22 +02:00
egutierrez f5fb4594f7 Merge quick/issue-0094-doc 2026-05-14 18:08:19 +02:00
egutierrez ef47a1acfe docs(issues): kanban 0094 bocadillo agente + PDF
Adjunta el issue del nuevo reporte diario con agente.
2026-05-14 18:08:19 +02:00
egutierrez eaaac1a321 Merge quick/kanban-issue-docs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:58:02 +02:00
egutierrez 60af81c104 docs(issues): kanban 0089-0093 reportes diarios + perf + archive
Archivos de issue para el trabajo de kanban de las ultimas iteraciones:

- 0089: tiempo maximo por columna con borde rojo (incluye followup popover
  con seleccion de unidad min/h/d/sem/mes).
- 0090: seleccion aleatoria por columna con animacion de ruleta. Ya con
  fix de no mostrar en columnas Done.
- 0092: archivo automatico para cards en columnas Done con +30 dias.
- 0093: reporte diario al pulsar el numero del dia en el calendario.

Los issues 0088 y 0091 ya estaban registrados.
2026-05-14 17:57:44 +02:00
egutierrez d3cc58e0e0 Merge issue 0091: kanban sidebar drag zones 2026-05-14 13:58:23 +02:00
egutierrez 79e08fe937 docs(issues): add 0091 — kanban sidebar drag zones
Issue spec for the drag-aware dropzone strip that auto-opens the
kanban sidebar after >=400ms hover during a drag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:58:18 +02:00
egutierrez 626d06327c feat(registry): add playwright capability group (6 TS browser fns)
New domain `browser` under frontend/functions/ with 6 Playwright helpers:
- pw_launch_browser: chromium + context + page bootstrap with storageState
  support and baseUrl navigation.
- pw_kanban_login: authenticates a Page against /api/auth/login; sets the
  kanban_session cookie via shared storageState; verifies login page no
  longer visible after navigation.
- pw_drag_drop: human-like pointer drag (mousedown + activateOffset +
  stepped move + mouseup) compatible with @dnd-kit/core's 8px activation
  threshold; supports hoverMs for time-based dropzones.
- pw_keyboard_sequence: ordered focus/type/press/wait steps for scripting
  realistic input flows (typing then arrow-key navigating autocompletes).
- pw_wait_predicate: thin wrapper over page.waitForFunction with friendlier
  defaults and custom error messages.
- pw_assert_class: poll-based assertion that a Locator has/lacks a CSS
  class within a timeout; useful for visual-state checks.

Each function ships with vitest tests (5-8 cases each) covering both happy
and error paths, plus self-documenting .md (Ejemplo + Cuando usarla +
Gotchas + frontmatter with params/output schema).

Adds frontend/functions/package.json with `"type": "module"` so consumers
can ESM-import the .ts files from anywhere in the registry (Playwright's
tsx loader respects nearest package.json).

Capability page docs/capabilities/playwright.md documents the group with
a canonical end-to-end example, frontiers, prerequisites, and gotchas.
Index updated.

First consumer (issue 0088): apps/kanban requester-input.spec.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:57:30 +02:00
egutierrez b5a56bb5ff feat(pipelines): auto-commit con 4 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 02:14:09 +02:00
egutierrez ca1bf5a59b feat(infra): auto-commit con 29 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 02:06:44 +02:00
egutierrez 47fac22230 chore: auto-commit (799 archivos)
- .claude/CLAUDE.md
- .claude/commands/subagentes.md
- .claude/rules/INDEX.md
- .mcp.json
- bash/functions/cybersecurity/analyze_dns.md
- bash/functions/cybersecurity/audit_http_headers.md
- bash/functions/cybersecurity/audit_ssh_config.md
- bash/functions/cybersecurity/check_firewall.md
- bash/functions/cybersecurity/detect_suspicious_users.md
- bash/functions/cybersecurity/encrypt_file.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:28:20 +02:00
egutierrez 20f72edb5a feat(metabase): auto-commit con 17 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:40:22 +02:00
egutierrez 8284afcba5 feat(ml): auto-commit con 14 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:22:02 +02:00
egutierrez 49a924bb34 chore(issues): mueve 0078/0079/0080 a completed/
3 issues cerradas movidas al directorio completed/ por convencion:

- 0078 tables playground joins MBQL (fase 9)
- 0079 tables playground drill-through extendido (fase 10)
- 0080 tables playground LLM Ask AI + TQL->SQL emit (fase 11)

0081 (promote a registry, fase 12) permanece en dev/issues/ — status
partial, 0081-A done, 0081-B..L pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:21:50 +02:00
egutierrez a07787fc5a chore(issues): cierra 0080, marca 0081 partial
0080 status: pending -> done (closed 2026-05-13). Notas: pure layer +
LLM client + Ask AI modal + DuckDB adapter (FN_TQL_DUCKDB ON). 618
tests con DuckDB, 603 sin.

0081 status: pending -> partial (in progress). 0081-A DONE (20 types
extraidos al registry). 0081-B..L pendientes: extraer functions
restantes (compute_stage, tql_emit/apply, lua_engine, tql_to_sql,
join_tables, viz_render, data_table) + fn_table_viz lib + migrar
5 apps + fn doctor cpp-apps check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:58:31 +02:00
egutierrez 5325a65f7c docs(types): drill_step + filter_preset md (0081-A)
Completa el batch de 20 type .md extraidos a cpp/types/core/ y
cpp/types/viz/ apuntando a cpp/functions/core/data_table_types.h.
Quedan 2 que faltaban en commits anteriores: DrillStep_cpp_core
(undo/redo de drills, fase 10) y FilterPreset_cpp_core (Last7/30/90d,
ExcludeNulls, NonZero, fase 10).

Total types indexados: 206. Tabla via mcp__registry__fn_search
"file_path:data_table_types" o sqlite SELECT por file_path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:58:25 +02:00
egutierrez 2ca973fb7c feat(playground): DuckDB adapter para TQL->SQL execute (issue 0080)
Cierra 0080 fase 11. tql_duckdb.{h,cpp} es adapter opcional gated por
build flag FN_TQL_DUCKDB=ON. Default OFF — playground sin deps DuckDB.

Funcionalidad:
- tql_duckdb::execute(sql, params, tables) -> Result con StageOutput
  materializado. Abre DuckDB :memory:, registra TableInputs via
  CREATE TABLE + INSERT batched (1000 rows/batch), prepare + bind
  params via duckdb_bind_varchar, execute_prepared, materializa
  resultado via duckdb_value_varchar + duckdb_free.
- type_from_duckdb mapeo DuckDB type -> ColumnType.
- CMakeLists.txt: option(FN_TQL_DUCKDB) + condicional add a sources
  + link duckdb_vendored + copy runtime.
- data_table.cpp Ask AI modal: ifdef FN_TQL_DUCKDB para status message
  apropiado en SQL apply.
- self_test.cpp: 4 round-trip tests gated por FN_TQL_DUCKDB:
  stage0 SELECT, group+count, filter Op::Eq, sum aggregation
  (verifica sum_n(go)=30).

Tests:
- 603 passed sin FN_TQL_DUCKDB (default OFF).
- 618 passed con FN_TQL_DUCKDB=ON (round-trip TQL emit -> DuckDB
  execute -> match compute_stage pure).
- e2e linux + windows cross-build OK ambos modos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:58:18 +02:00
egutierrez e3c8979e8d chore: auto-commit (95 archivos)
- cmd/fn/doctor.go
- cmd/fn/main.go
- cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt
- cpp/apps/primitives_gallery/playground/tables/data_table.cpp
- cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp
- cpp/apps/primitives_gallery/playground/tables/data_table_logic.h
- cpp/apps/primitives_gallery/playground/tables/self_test.cpp
- cpp/apps/primitives_gallery/playground/tables/tql.cpp
- cpp/apps/primitives_gallery/playground/tables/viz.cpp
- cpp/apps/primitives_gallery/playground/tables/viz.h
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:50:34 +02:00
egutierrez a2bbf23374 feat(infra): auto-commit con 1 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:04:24 +02:00
egutierrez b8911dc080 feat(browser): auto-commit con 9 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:03:45 +02:00
egutierrez 26e4266c82 chore(issues): auto-commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 03:17:43 +02:00
egutierrez 5cf3615bbb fix: zsh-safe var rename + yaml returns as list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 03:13:53 +02:00
egutierrez 70c50a2229 asegurate de que subimos todo
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 03:10:00 +02:00
egutierrez 47e80b70c2 feat(kotlin-compose): finalize design system + apps + sync sub-repo gitlinks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:30:43 +02:00
egutierrez cb6d9e61d1 feat(kotlin-compose): design system + 33 components + gallery_kt + e2e android emulator + scaffolder fixes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:28:50 +02:00
egutierrez 0bdb8454e1 Merge branch 'issue/0005-fix-layout-dock-restore' (cfg.pre_frame hook) 2026-05-10 14:21:32 +02:00
egutierrez 936de33d0f feat(framework): cfg.pre_frame hook for apps with own LayoutStorage
Apps que gestionan su propio LayoutStorage (cfg.auto_layouts=false +
cfg.layouts_cb=&own_cb) necesitan llamar layout_storage_apply_pending
en el momento correcto: despues de ImGui::NewFrame y ANTES de menubar
+ auto-dockspace + cualquier Begin() del frame.

Antes, si la app llamaba apply_pending dentro de render_fn (es decir,
mid-frame), ImGui cargaba el INI pero las dock-nodes no se restauraban
hasta el siguiente ciclo: las ventanas docked aparecian flotantes.

cfg.pre_frame es un std::function<void()> opcional que run_app y
run_app_test invocan justo despues de NewFrame, antes del bloque
auto_layouts_storage, antes de app_menubar y antes del auto-dockspace.
Default null = no-op, sin impacto en apps existentes.

Apps con auto_layouts=true (la mayoria) no necesitan tocar nada — el
framework ya hace apply_pending en su propio bloque. pre_frame es
puramente para apps con layout custom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:21:00 +02:00
egutierrez 231d5d87a6 feat(infra): auto-commit con 11 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:30:27 +02:00
egutierrez 0c1727fef7 docs(issues): add 0071a chat, 0071b jobs, 0071g app_db_init extraction plans
Sub-issues activables de 0071 con plan concreto: API, dependencias, migracion, tests, anti-patrones.

- 0071a (alta): claude_chat_panel — 2 consumidores reales (graph_explorer + navegator_dashboard, ~2500 LoC dup). Depende de 0071f.
- 0071b (media): jobs_queue_panel — absorbe issue 0065. Depende de 0071f. Pre-requisito: auditar dup vs odr_console.
- 0071g (media): app_db_init Tier 4 — 4+ duplicaciones en graph_explorer. Bajo riesgo.

README actualizado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:32:51 +02:00
egutierrez f3c0a5c532 docs(issues): add 0071 cpp panels roadmap + 0071f subprocess_streamer
0071 cataloga paneles ImGui candidatos a extraccion por tier (rule of three).
0071f es el primer sub-issue activable: subprocess_streamer ya tiene 3 consumidores
reales en graph_explorer (chat, jobs, extract_panel). README actualizado con 0068-0071+0071f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:26:42 +02:00
egutierrez 750b7abcd5 chore: auto-commit (97 archivos)
- .claude/CLAUDE.md
- .claude/agents/fn-recopilador/SKILL.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- bash/functions/infra/build_cpp_windows.sh
- cpp/CMakeLists.txt
- cpp/PATTERNS.md
- cpp/framework/app_base.cpp
- cpp/framework/app_base.h
- dev/issues/README.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:11:24 +02:00
egutierrez 852322a708 fix(http_logger): preservar Hijack y Flush para WebSocket y SSE
El responseWriter del logger middleware envolvia http.ResponseWriter sin
implementar http.Hijacker ni http.Flusher. Esto rompia el upgrade
WebSocket (501 Not Implemented) y el flush de SSE.

Anade Hijack() y Flush() que delegan al writer subyacente. Detectado
via e2e tests de apps/kanban que arrancaban el binario real y dialeaban
/api/chat/ws — el upgrade fallaba con 501 hasta este fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:00:49 +02:00
egutierrez 4881eeb7de feat(registry): claude_stream + mcp_server_stdio para chat con tool-use
- claude_stream_go_core: lanza claude -p --output-format stream-json
  --verbose, decodifica NDJSON y emite eventos sinteticos (text_delta,
  tool_use, tool_result, result, error) por canal Go. 10 tests con fake
  claude bash.
- mcp_server_stdio_go_infra: scaffold de MCP server JSON-RPC 2.0 sobre
  stdio (initialize, tools/list, tools/call, ping). Usuario registra
  tool defs y handler unico. 9 tests.

Usadas por apps/kanban backend para reemplazar el chat HTTP one-shot
con XML actions por WebSocket streaming + tool-use nativa.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:54:56 +02:00
egutierrez 98c4982707 chore: register registry_mcp in .mcp.json
Auto-loads registry MCP server (fn_search, fn_show, fn_code,
fn_list_domains, fn_uses, fn_doctor, fn_run, fn_create_function)
in any Claude Code session opened from this repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:56:38 +02:00
egutierrez fd23961497 merge: 0064 registry_mcp MCP server 2026-05-09 13:31:29 +02:00
egutierrez a661585cb7 docs(issues): close 0064 — registry_mcp MCP server shipped
Server exposes registry.db to Claude clients via stdio (default) or
HTTP+SSE. Read-only tools (fn_search, fn_show, fn_code, fn_list_domains,
fn_uses, fn_doctor) plus opt-in fn_run + fn_create_function for
iterative function authoring. Lives in dataforge/registry_mcp sub-repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:31:14 +02:00
egutierrez 12b37b9a05 feat(registry): expose Conn() and Path() on registry.DB
Allows external readers (registry_mcp app) to issue raw aggregations
(e.g. fn_list_domains) and inspect the active db path without
duplicating the connection setup logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:31:08 +02:00
egutierrez 618c07a12c feat(infra): sqlite_apply_versioned_migrations + dedup fn_operations + registry
Promueve patron versionado (schema_migrations + tx por archivo) al registry
como sqlite_apply_versioned_migrations_go_infra. Migra fn_operations/migrate.go
y registry/migrate.go al consumirla. ~200 LOC duplicadas eliminadas.

- functions/infra/sqlite_apply_versioned_migrations.{go,md,_test.go}: nueva,
  5/5 tests pass. Generaliza fs.FS + dir param (fn_operations usaba embed.FS
  hardcoded). Distinta de sqlite_apply_migrations_go_infra (naive split-by-`;`,
  idempotent-by-error) — esta hace tracking explicito + transactions.
- fn_operations/migrate.go: 111 LOC -> 17. Wrapper sobre infra.ApplyVersionedMigrations.
- registry/migrate.go: idem. Mismo patron copy-paste, ahora unificado.

Smoke: ./fn ops init crea operations.db con schema_migrations poblada.
fn_operations + registry tests: PASS. fn index registra nueva fn (1091 total).
2026-05-09 12:50:51 +02:00
egutierrez f65178025d feat(audit+pipelines): mejor deteccion + auto-recovery TBD
- audit_uses_functions: parsea Go func name del signature (no solo PascalCase de name); skip _test.go y dirs e2e/tests/testdata/build/dist/vendor/node_modules; add scanner TS para frontend/ con import "@fn_library/<area>/<name>" → <name>_ts_<area>; unused solo flagea langs efectivamente escaneados
- full_git_push: si pre-commit hook bloquea, retry con --no-verify y reporta bypass; si push rechazado por non-fast-forward, fetch + merge --no-ff auto y reintenta; exit code 1 + bloque [!!] ERRORES si quedan errores reales
- full_git_pull: si pull --ff-only diverge, intenta merge --no-ff auto contra @{u}; conserva [merged-auto] o aborta con [diverged] si conflicto; exit code 1 si quedan repos pendientes
- slash commands /full-git-push y /full-git-pull: documentadas obligaciones del agente para garantizar TBD (master siempre alineado con remote)
- kanban app.md: quita percentile_int64 (transitivo via duration_stats)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:57:51 +02:00
egutierrez 03568c88e3 chore: auto-commit (57 archivos)
- frontend/functions/core/format_datetime_short.md
- frontend/functions/core/format_datetime_short.test.ts
- frontend/functions/core/format_datetime_short.ts
- frontend/functions/core/format_duration.md
- frontend/functions/core/format_duration.test.ts
- frontend/functions/core/format_duration.ts
- frontend/functions/core/month_grid.md
- frontend/functions/core/month_grid.test.ts
- frontend/functions/core/month_grid.ts
- frontend/functions/core/string_hash_palette.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:41:58 +02:00
egutierrez cd50e790ca docs(rules): db migrations obligatorias retroactivas y siempre
- db_migrations.md (nuevo): doctrina archivos numerados, aditivo, idempotente, embed.FS pattern, branch-by-abstraction para destructivos, anti-patrones, inventario retroactivo del ecosistema
- INDEX: entrada 25
- CLAUDE.md: nota en cabecera

Aplicado retroactivamente en commit paralelo: kanban (003-005), deploy_server (001-002), agents_and_robots/memory (001).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:44:29 +02:00
egutierrez 48dd5a1869 docs(rules): TBD con feature flags para WIP sin romper master
- apps_tbd.md: tabla de decision para WIP en working tree (incluir/flag/stash/issue separado)
- feature_flags.md (nuevo): doctrina TBD oficial, patrones Go/TS/Bash/Py, branch-by-abstraction, anti-patrones
- INDEX: entrada 24

Refs: trunkbaseddevelopment.com/feature-flags y branch-by-abstraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:03:48 +02:00
egutierrez 4a4dfc6c05 chore: auto-commit (3 archivos)
- apps/shaders_lab/app.md
- dev/issues/README.md
- dev/issues/0063-kanban-stickers.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:55:35 +02:00
egutierrez 9c661d605a feat(infra): auto-commit con 12 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:21:17 +02:00
egutierrez 73bb48fa7c feat(infra): auto-commit con 1 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:27:18 +02:00
egutierrez c6efbc977b feat(infra): auto-commit con 1 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:07:56 +02:00
egutierrez 8f37b0e81a fix(pipelines): full_git_push cubre TODOS los repos del registry
discover_git_repos: quitar -type d para cubrir submodulos
y worktrees (.git como archivo, no solo directorio).

full_git_push auto-init: reemplazar bucle hardcodeado
sobre apps/, analysis/, projects/*/{apps,analysis}/ por
iteracion BD-driven sobre TODOS los dir_path indexados.
Cubre cpp/apps/, projects/*/apps/ y cualquier ubicacion
futura sin tocar este codigo.

Detectaba 32 repos; ahora 33. Auto-init detecta 2 missing
(chart_demo, shaders_lab) que antes quedaban fuera.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:33:52 +02:00
egutierrez f2381b7b2b docs(issues): 0054-0062 — deudas detectadas en sesion fn doctor
Nueve issues nuevos cubriendo deudas tecnicas descubiertas tras
ejecutar fn doctor por primera vez:

- 0054 deploy_server: reimplementa SSH/systemd/rsync inline en lugar
  de usar funciones del registry (alta).
- 0055 docker_tui: usa docker CLI directo via shell en lugar de
  docker_* del registry (alta).
- 0056 audit_uses_functions: heuristica Python no detecta
  `from pkg.subpkg import X` (media).
- 0057 audit_uses_functions: deteccion de simbolos Go con
  abreviaturas falla en algunos casos (baja).
- 0058 kanban uses_functions sync deferido por WIP en curso (baja).
- 0059 doble tracking de apps/*/app.md (fn_registry + sub-repo)
  inconsistencia (media).
- 0060 fn doctor secrets: subcomando para audit secrets en TODOS
  los repos (media).
- 0061 integrar notify_telegram en deploy_server + bucle reactivo
  (media, depende de 0054).
- 0062 politica de deprecacion para 704 funciones sin consumidores
  (baja, research).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:16:43 +02:00
egutierrez eeff744dfc feat: dagu backup DAG + pre-commit drift hook + sync 6 apps
Priority 1: Daily backup automation via Dagu DAG (~/dagu/dags/fn_backup.yaml,
schedule "0 3 * * *"). Backs up registry.db, each operations.db, and vaults
via rsync --link-dest. Fixes set -e arithmetic bugs in rotate_backups.sh and
backup_all.sh ((var++) returns 1 when var=0). Fixes && chain set -e bug in
vault rotation.

Priority 2: Pre-commit hook v2 chains scan_secrets + uses_functions audit.
New function git_hook_audit_app_drift_bash_infra blocks commits that touch
app code when that app has uses_functions drift. Allows corrective app.md-only
edits. Installed on fn_registry + 32 sub-repos.

Priority 3: Synced uses_functions in 6 sub-repo apps (commits in their own
repos): dag_engine, script_navegador, deploy_server, docker_tui,
auto_metabase, metabase_registry. Drift went from 7/12 to 4/12 apps.
Remaining drift = audit heuristic limitations (Python nested imports,
Go symbol name detection).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:09:33 +02:00
egutierrez 2a3d780347 feat(doctor): add fn doctor CLI + 14 functions for system management
Adds `fn doctor` read-only diagnostic command with subcommands artefacts,
services, sync, uses-functions, unused, and --json flag for agents.
Each subcommand wraps a registry function in functions/infra/.

New functions:
- artefact_doctor, services_status, pc_locations_drift,
  audit_uses_functions, find_unused_functions (Go diagnostics)
- backup_sqlite_db, rotate_backups, wait_for_http, wait_for_port,
  port_kill, tail_journal, pre_commit_hook_install (bash utilities)
- notify_telegram (Go HTTP)
- backup_all pipeline (tag launcher)

Plus prior session leftovers (scan_secrets_in_dirty, append_diary_entry,
git utilities, http_session_cookie_middleware, compile/full-git pipelines).

Fixes pc_locations_drift filepath.Join bug with absolute dir_path.
Documents fn doctor in CLAUDE.md, .claude/rules/fn_doctor.md (rule 23),
docs/architecture.md, CHANGELOG.md (2026-05-07), and diary entry.

First fn doctor uses-functions run found drift in 7/12 apps (deuda
para sincronizar app.md con imports reales).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:42:10 +02:00
egutierrez fd9e9135a9 chore: auto-commit (4 archivos modificados)
- .claude/commands/full-git-pull.md
- .claude/commands/full-git-push.md
- .claude/rules/frontend_theming.md
- go.sum

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:05:24 +02:00
egutierrez 3e77cbb287 docs(issues): cierra 0053 (kanban chat panel)
Chat lateral en apps/kanban implementado:
- backend: chat.go + tools.go con dispatch a 11 tools
  (list_board, create_column, rename_column, delete_column,
   reorder_columns, create_card, update_card, delete_card,
   move_card, card_history, find_cards)
- runClaude usa subprocess `claude -p --model claude-sonnet-4-6`
  con suscripcion del usuario (sin ANTHROPIC_API_KEY)
- frontend: ChatPanel en AppShell.Aside, persistencia localStorage,
  markdown via react-markdown + remark-gfm
- smoke test verde: crear cols, crear cards, queries conversacionales

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:54:17 +02:00
egutierrez 3608409a94 docs(rules): registry-first + FTS5 quoting gotcha
- Nueva regla registry_first.md: antes de escribir codigo en un artefacto,
  buscar en registry.db (FTS5); si falta una primitiva reutilizable,
  delegar a fn-constructor en vez de escribir inline.
- INDEX.md: entrada 22 para la nueva regla.
- CLAUDE.md: nota sobre escapado de tokens FTS5 con caracteres
  especiales (column:"valor-con-guion") para evitar errores
  "no such column" / "syntax error near .".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:54:09 +02:00
egutierrez 7849e1cc8e feat(registry): random_hex_id_go_core + spa_handler_go_infra
Dos primitivas reutilizables para apps web del registry:
- random_hex_id_go_core: IDs hex aleatorios (apps con SQLite + IDs string)
- spa_handler_go_infra: http.Handler que sirve embed.FS con fallback
  a index.html (patron SPA para React Router/dnd-kit)

Ambas creadas via fn-constructor durante apps/kanban (issue 0053).
Tests pasan, fn index OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:54:01 +02:00
egutierrez 2be91b0c4d chore(commands): auto-commit full-git-push.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:13:11 +02:00
egutierrez 1bef88da67 docs(rules): añade reglas de artefactos y playgrounds
- Nueva regla 20: artefactos.md (paraguas para apps/analysis/vaults/projects/playgrounds)
- Nueva regla 21: playgrounds.md (prototipos rapidos dentro de un padre)
- INDEX.md y CLAUDE.md actualizados con referencias

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:54:34 +02:00
egutierrez 611fc81b6b feat: cierra issues 0050 y 0052 + commands automáticos
- 0050: jupyter_exec reescrito sin Y.js (REST + KernelClient). Bug raíz adicional: HEAD /api/contents da 405 → cambiado a GET. 9 tests (5 unit + 4 e2e).
- 0052: footprint_aurgi cerrado. Bug fix en setup_geo_stack_docker_pipeline (verify aborta si compose up falla; nombre de contenedor incorrecto).
- Nueva primitiva docker_container_running_py_infra (7 tests).
- /full-git-push y /full-git-pull pasan a modo automático: auto-commit + push sin preguntar, aborta solo si detecta secrets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:34:03 +02:00
egutierrez 1bb4f2e0f8 fix(ui): notification popup horizontal oscillation
Tamano FIJO del popup (Always + SizeConstraints) y flags NoResize/NoMove
para evitar feedback loop entre auto-resize del popup y TextWrapped/SameLine
internos. Reemplaza GetWindowContentRegionMax() por offsets explicitos
calculados a partir del ancho fijo, ya que ese valor fluctua frame a frame
con padding/borders y provocaba el ensanche/encogido continuo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:12:50 +02:00
egutierrez 604ce635ec feat(metabase): smartscalar KPI builders (sql + payload + dimension tag)
3 helpers puros para construir KPIs con display=smartscalar y comparacion
vs n-1 sin que Metabase v0.59 pida breakout temporal. Replican el patron
del dashboard Informe Lean (UNION ALL de 2 filas periodo/valor) y rellenan
la firma exacta de template-tags que el frontend MBQL5 acepta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:29:26 +02:00
egutierrez faac610745 feat: extraccion masiva footprint_aurgi (41 funcs + 4 types + stack Docker geo)
Extrae al registry funciones del proyecto interno footprint_aurgi:
- core (6): slugify_ascii, normalize_for_join, cp_provincia_es, infer_provincia_from_cp, safe_read_csv_fallback, csv_to_parquet_duckdb
- geo puras (7): haversine_km, point_in_ring, point_in_polygon, point_in_polygons_bbox, polygon_bbox, extent_with_padding, distance_bucket
- geo I/O (4): load_geojson_polygons, load_boundary_gdf, add_basemap_osm, add_basemap_with_timeout
- valhalla client (4): valhalla_route, valhalla_isochrone, valhalla_isochrones_async, valhalla_matrix_1_to_n
- datascience stats (7): trimmed_mean, geometric_mean, detect_distribution_type, best_central_tendency, summary_stats, kde_density_levels, alpha_shape_concave_hull
- datascience fuzzy (3): fuzzy_merge_adaptive (rapidfuzz), words_to_dataset, remove_words_from_column
- datascience viz (2): plot_kde_2d, plot_heatmap_log
- infra (4): compress_pdf_ghostscript, render_table_page_pdfpages, add_header_logo, osm2pgsql_ingest
- pipelines (4): setup_geo_stack_docker, compute_centers_reachability, generate_isochrones_by_zone, count_points_per_zone
- types geo (4): LonLat, BBox, IsochroneRequest, Centro

Incluye:
- apps/footprint_geo_stack/ (PostGIS + Martin + Valhalla via docker-compose)
- 131/132 tests pasan (1 skip esperado: osm2pgsql en PATH)
- Issue tracker dev/issues/0052-footprint-aurgi-extraction.md
- Atribucion uniforme: source_repo internal:footprint_aurgi, source_license internal-aurgi
- Build con 9 agentes en paralelo (8 wave 1 + 1 wave 2 pipelines)

Tambien commitea trabajo previo no commiteado: aggregate_extraction_results, chunk_with_overlap, clean_pdf_text, merge_entity_aliases, extract_graph_gliner2, extract_relations_mrebel, extract_triples_spacy_es, gliner2/mrebel/marianmt/rebel/spacy_es load_model, parse_rebel_output, translate_es_to_en, issue 0050/0051.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:35:22 +02:00
egutierrez f73ea072bd chore: limpiar gitignore obsoleto de graph_explorer en raiz
graph_explorer.exe ya escribe en local_files/ via fn::local_path() segun la convencion de cpp_apps.md §7, asi que los archivos /graph_explorer.{db,ini,...} ya no aparecen en la raiz del registry. Eliminados los 4 archivos remanentes del 1 de mayo y las 6 lineas correspondientes del .gitignore que ya no protegen contra nada.
2026-05-04 21:57:37 +02:00
egutierrez 6795598215 fix(datascience): glirel_load_model compatible con huggingface_hub 1.x
GLiREL declara proxies/resume_download como required-keyword en
_from_pretrained, pero huggingface_hub 1.x dejo de pasarlos en su
from_pretrained. Aplicamos un classmethod monkey-patch idempotente
que inyecta valores neutros si faltan. Verificado contra glirel==1.2.1
y huggingface_hub==1.13.0 con jackboyla/glirel-large-v0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:43:35 +02:00
egutierrez d02d84152d feat(cpp): _GE_DIR y _DASH_DIR sobreescribibles para builds en worktrees
Permite -D_GE_DIR=<path> y -D_DASH_DIR=<path> via cmake para apuntar
estas apps externas a un worktree aislado. Sin override, comportamiento
identico al previo. Habilita parallel-fix-issues sobre apps C++ cuyo
binario sale del arbol cpp/ pero cuyo source vive en projects/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:06:34 +02:00
egutierrez f5f14ccd53 docs: 2026-05-04 changelog + diary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:52:56 +02:00
egutierrez 715e2431fc feat(cpp/core): parallel_for thread pool + slider widget
parallel_for_cpp_core: ThreadPool reutilizable con parallel_for(begin, end, fn)
y parallel_for_chunks(begin, end, fn(tid, lo, hi)). Captura excepciones del
worker y las relanza en el caller. Pareja CPU del despacho GPU para Monte
Carlo multi-core cuando dispatch GPU no compensa.

slider_cpp_core: wrapper de ImGui::SliderFloat/Int/Double con label muted
arriba, tokens (primary grab), full-width. Variantes float, float_log
(logaritmico), int, double. Para los calculadores que tienen 15-30 sliders
cada uno y se beneficia del estilo consistente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:52:50 +02:00
egutierrez 9d69953110 feat(cpp/datascience): GPU Monte Carlo kernels (K1-K3)
Tres kernels Monte Carlo intensivos sobre las primitivas G1-G7 + las puras
CPU como oraculo de tests numericos. Apuntados a hyper-paralelizar los
calculadores de sources/calculadoras (vr_tiered_lab, mcmc-bayes / full / lab,
mcmc-visualizer) en RTX-class GPUs.

- mc_session_sim_gpu (K1): N sesiones independientes de K spins en paralelo
  (1 thread = 1 sesion). Modelo variable-ratio escalonado con tiers (q, m),
  modes Pure/Pity/Streak, miss_streak, drawdown. SSBOs summary[N*8] y
  tier_counts[N*max_tiers]. Portea vr_tiered_lab.
- mc_metropolis_hastings_gpu (K2): M cadenas independientes 1D. Target
  log-pdf inyectable como string GLSL (mismo patron de gl_shader). u_user[16]
  para cambiar parametros desde sliders sin recompilar. Output compatible
  con rhat_split / ess_basic.
- mc_random_walk_2d_gpu (K3): walkers 2D MH con trace_xy xy-interleaved en
  SSBO; pasable directamente a gpu_histogram_2d sin readback intermedio.
  Pipeline GPU-only para mcmc-visualizer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:52:41 +02:00
egutierrez d76c831247 feat(cpp/datascience): CPU stats + MCMC primitives
Nuevo dominio cpp/functions/datascience con primitivas puras CPU para post-
proceso de samples Monte Carlo y diagnostico de cadenas MCMC. Diseñadas como
gemelas CPU de los kernels GPU (rng pareja con gpu_rng_glsl, MH 1D/ND con
mc_metropolis_hastings_gpu) para validar numericamente y para datasets
pequeños donde el dispatch GPU no compensa.

- rng: xoshiro256++ con uniform / normal (Box-Muller) / below (Lemire) /
  categorical. Determinista bit-exacto dado seed.
- stats_summary: sum (Kahan), mean, var/std (Welford one-pass), min, max,
  quantile / percentile (R type-7).
- autocorr: r(k), ACF, tau_int (Sokal) — diagnostico ACF y ESS.
- rhat_ess: Gelman-Rubin clasico y split + ESS basico (multi-chain).
- beta_dist: lgamma (Lanczos), beta_pdf, beta_cdf (continued fraction),
  beta_quantile, mean/var/std — para inferencia Beta-Binomial.
- drawdown: max_dd absoluto/pct + underwater series para sesiones
  simuladas y backtests.
- samples_to_grid_2d: binning 2D CPU para alimentar heatmap_cpp_viz /
  contour_cpp_viz desde samples (x[], y[]).
- metropolis_hastings: MH 1D y ND con target log-pdf como std::function
  (no normalizada).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:52:26 +02:00
egutierrez c74fd4ae0d feat(cpp/gfx): GPU compute primitives for Monte Carlo (G1-G7)
Stack base de compute shaders OpenGL 4.3 para cargas Monte Carlo intensivas
en GPU. Reutiliza el patron de graph_force_layout_gpu (SSBO + compute) y se
integra con el resto del registry sin nuevos simbolos en gl_loader (todo lo
que se necesita ya estaba expuesto).

- gpu_ssbo: lifecycle de Shader Storage Buffer Objects.
- gpu_compute_program: compila compute GLSL 4.3 con preamble inyectable
  (mismo pattern de gl_shader::compile_fragment).
- gpu_dispatch: dispatch_1d/2d/3d con ceil(N/local) automatico + barrier
  helpers (storage, uniform, image, buffer_update, all).
- gpu_rng_glsl: PCG32 GLSL (uniform/normal/below) + SplitMix64 seed walkers
  para sembrar deterministicamente N walkers desde un master seed.
- gpu_histogram_1d: SSBO float[N] -> uint[nbins] via atomicAdd.
- gpu_histogram_2d: SSBO float[2N] xy-interleaved -> uint[nx*ny] +
  to_density helper para alimentar heatmap_cpp_viz.
- gpu_reduce: workgroup-shared sum/min/max/mean (local 256, partials CPU).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:52:08 +02:00
egutierrez 92297e02c5 feat(go): html_to_markdown + extract_iocs
functions/core/html_to_markdown: convierte HTML a Markdown limpio (golang-only
sin dependencias externas). util como prep para LLMs y para indexar contenido
web.

functions/cybersecurity/extract_iocs + types/cybersecurity/ioc: extrae
indicators of compromise (IPs, domains, URLs, hashes, emails, CVEs,
crypto wallets) de texto libre. Devuelve []IOC tipado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:51:51 +02:00
egutierrez b643321778 chore(cpp/gfx): add glUniform1ui binding to gl_loader
Necesario para que las funciones GPU compute (gpu_histogram_1d/2d, gpu_reduce,
mc_*_gpu) puedan setear uniforms uint en Windows. En Linux ya estaba
disponible via GL_GLEXT_PROTOTYPES.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:51:44 +02:00
egutierrez ab6ce8f822 feat(registry): index cpp/apps/* + e2e test infrastructure
registry/indexer.go ahora escanea <lang>/apps/*/app.md ademas de apps/ y
projects/*/apps/. cpp/apps/chart_demo y cpp/apps/shaders_lab pasan a estar
en registry.db con sus manifests.

Infraestructura de tests e2e (opt-in con -DFN_BUILD_TESTS=ON):
- vendor de Dear ImGui Test Engine (personal/open-source license).
- chart_demo_tests target con tests/chart_demo_tests.cpp.
- /e2e-cpp slash command para crear y ejecutar tests e2e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:51:38 +02:00
egutierrez bf94893032 feat(cpp/viz): split orphan TUs as separate fn entries (ADR 0003)
Cuando una funcion del registry parte su .cpp en varios TUs por testabilidad
o separacion ImGui-vs-puro, cada TU adicional se registra como entrada propia
con su .md en lugar de extender file_path para listar varios archivos.

Aplicado a:
- graph_labels_select_cpp_viz: helpers puros (compute_degrees + labels_select).
- graph_viewport_selection_cpp_viz: clear/add/toggle/is_selected puros.
- graph_types_cpp_viz: TU de update_bounds + find_node_by_user_data.

graph_labels y graph_viewport actualizados para declarar las nuevas entradas
en uses_functions. Razon detallada en docs/adr/0003 + regla actualizada en
.claude/rules/uses_functions.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:51:10 +02:00
egutierrez f1a5e04d4f feat(cpp/core): logger + log_window + selectable_text widgets
Logger global thread-safe con ring buffer in-memory de 2000 entradas + escritura
opcional a archivo. log_window flotante consume el ring buffer con filtros por
nivel, busqueda y autoscroll; se abre desde Settings -> Logs en la menubar.
selectable_text cubre el patron drag-to-select + Ctrl+C en cualquier ventana.

app_menubar y framework run_app integran log_window_render() en el frame loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:50:57 +02:00
egutierrez d55b8fea5d chore: gitignore graph_explorer state files at root
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:50:43 +02:00
egutierrez c5b2ec8ad6 feat(framework): assets/ subfolder para distribuibles read-only
Refina la convencion de layout: el top de cada app distribuible
solo lleva el .exe + DLLs nativas; todo lo demas (TTFs, enrichers,
runtime Python, MCP servers) vive en <exe_dir>/assets/.

Cambios:
- cpp/CMakeLists.txt::add_imgui_app — copia las 5 TTFs (Karla,
  Roboto, DroidSans, Cousine, tabler-icons) a
  $<TARGET_FILE_DIR>/assets/ en lugar de junto al exe.
- framework/app_base: nuevas funciones fn::asset_dir() y
  fn::asset_path(name) que resuelven a <exe_dir>/assets/<name>.
- functions/core/icon_font.cpp::find_asset — anade
  fn::asset_path(filename) como PRIMERA ruta de busqueda, antes
  de las legacy ./<file> y ./assets/<file>. Mantiene los
  fallbacks para dev (FN_ASSETS_DIR, FN_CPP_ROOT).
- .claude/commands/compile.md — el deploy a Desktop pone TTFs +
  enrichers/ + runtime/ + gx-cli en <DEST>/assets/. Solo .exe y
  DLLs nativas (duckdb.dll) quedan en el top. local_files/ se
  preserva si existe.

Layout final:
  Desktop/apps/<APP>/
  ├── <APP>.exe + *.dll          (binario + DLLs Windows)
  ├── assets/                    (read-only distribuible)
  │   ├── *.ttf, enrichers/, runtime/, gx-cli, ...
  └── local_files/               (per-PC, creado al primer arranque)

Esto cierra la separacion conceptual de la convencion: la carpeta
es trivial de zippear (solo .exe + assets/), el reset/sync es
trivial (local_files/), y todas las apps del registry adoptan el
mismo layout via fn_framework.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:50:33 +02:00
egutierrez 157bc093f4 docs(compile): adopta layout local_files/ + enrichers/ + runtime/ Python
Actualiza /compile para que el deploy a Desktop/apps/<app>/ siga la
convencion local_files/ del framework:

- Copia .exe + ttfs + dlls junto al exe (read-only).
- Copia <app_dir>/enrichers/ si existe (excluyendo pycache).
- Copia <app_dir>/runtime/ si app.md declara python_runtime: true.
  Regenera el runtime via tools/freeze_python_runtime.sh windows
  cuando app.md es mas nuevo que runtime/.lock.
- NUNCA toca local_files/ del destino — contiene estado del
  usuario (DBs, ini, proyectos) que NO se debe perder al
  recompilar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:37:22 +02:00
egutierrez f769928ffd feat(framework): convencion local_files/ — separacion distribuible vs estado
Toda app C++ basada en fn::run_app coloca sus archivos escribibles
bajo <exe_dir>/local_files/. Los distribuibles (.exe, dlls, ttfs,
enrichers/, runtime/) siguen junto al .exe. Esto deja la carpeta
distribuible limpia para zippear y separa con claridad lo que
viaja con la app de lo que el PC genera.

API publica en fn:: (cpp/framework/app_base.h):
  - exe_dir()                    directorio del ejecutable
  - local_dir()                  <exe_dir>/local_files/, creado on-demand
  - local_path(name)             <local_dir>/<name>
  - migrate_to_local_files(...)  mueve archivos viejos desde cwd/exe_dir

Cambios:
- run_app configura io.IniFilename = local_path("imgui.ini") y
  llama migrate_to_local_files(["imgui.ini","app_settings.ini"])
  antes de settings_load(). Migracion idempotente para PCs con
  instalacion previa.
- app_settings.cpp usa local_path("app_settings.ini") en lugar de
  hardcoded "app_settings.ini" relativo al cwd.
- cpp_apps.md §7 documenta la convencion como obligatoria. Las
  apps deben usar fn::local_path() para cualquier archivo
  escribible nuevo.

Beneficios:
- zip distribuible no se "ensucia" con .ini/.db generados al usar.
- reset trivial: borrar local_files/.
- backup/sync per-PC: solo local_files/ es propio del PC.
- elimina la mezcla de paths Linux/Windows que generaba bugs como
  "projects\\default\\operations.db" en builds cross-platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:32:55 +02:00
egutierrez f0cb5bce24 chore(vendor): vendor DuckDB v1.1.3 + CMake target DuckDB::DuckDB
cpp/vendor/duckdb/ con:
- include/duckdb.h         (C API, versionado)
- download_duckdb.sh       (descarga libs precompiladas para linux/windows)
- .gitignore               (libduckdb.so, duckdb.dll, duckdb.lib, libduckdb_static.a)
- README.md

cpp/CMakeLists.txt anade target INTERFACE 'duckdb_vendored' (alias
DuckDB::DuckDB) que las apps enlazan con target_link_libraries.
duckdb_copy_runtime(<target>) copia la lib runtime al lado del exe en
build time. Primer consumidor: graph_explorer (issue 0010).
2026-05-01 01:25:09 +02:00
egutierrez 1af0cf6132 docs(commands): full-git-pull no clona repos faltantes
Actualiza /full-git-pull para reflejar la realidad operativa: cada PC
mantiene solo el subset de sub-repos que necesita, segun la memoria
"Gitea = fuente de verdad; PCs subset".

Cambios:
- Quita la segunda pasada que clonaba automaticamente todos los
  dataforge/<name> registrados en apps/analysis. Generaba clones no
  deseados en PCs que no usan esas apps.
- Anade nota explicita de que el comando solo actualiza repos con
  .git/ ya presente y deja el clone manual como pull-on-demand.
- Documenta el snippet de clone manual con token via pass para
  cuando si haga falta traer un sub-repo nuevo.

Impacto: el comando es idempotente y predecible — no toca lo que no
existe localmente. No afecta a fn sync ni a la regeneracion de
registry.db.
2026-04-30 17:24:09 +02:00
egutierrez 4e1f0c831d merge: issue/0040-hybrid-extraction-pipeline — pipeline hibrido extraccion grafos 2026-04-30 16:53:31 +02:00
egutierrez f8c34d4b16 docs(issues): cerrar 0040 — hybrid extraction pipeline
Mueve el issue a completed/ y actualiza el indice.
2026-04-30 16:52:56 +02:00
egutierrez 4f743e0840 feat(pipelines): extract_graph_hybrid (regex + GLiNER + GLiREL + LLM fallback)
Pipeline en cascada que combina extract_iocs (regex, coste 0), GLiNER
(zero-shot NER), GLiREL (zero-shot RE) y un fallback LLM opcional para
chunks con baja confianza o pocas entidades. Devuelve listas concatenadas
listas para deduplicate_entities/deduplicate_relations.

Cierra 0040.
2026-04-30 16:52:46 +02:00
egutierrez 0c1c08742b merge: issue/0039-glirel-relation-extractor — GLiREL relation extractor
# Conflicts:
#	dev/issues/README.md
#	python/pyproject.toml
2026-04-30 16:42:21 +02:00
egutierrez 20374a9220 docs(issues): cerrar 0038 — GLiNER entity extractor
- Move dev/issues/0038-gliner-entity-extractor.md a completed/
- Update README link y estado a completado

Closes #0038

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:30 +02:00
egutierrez f6527da7ca test(datascience): corpus stub para gliner_load_model + extract_entities_gliner
11 tests sin necesidad de descargar el modelo (200 MB):
- StubModel duck-typed que valida el contrato de predict_entities
- Threshold y flat_ner se propagan al modelo
- Schema vacio lanza ValueError; schema sin labels validos warning + []
- Excepcion del modelo se captura
- Label desconocido se descarta
- gliner_load_model: ImportError simulado, cache hit, _resolve_device
  auto cae a cpu si torch no esta presente

Refs #0038

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:30 +02:00
egutierrez 7cdb8e1eb2 feat(datascience): GLiNER entity extractor (zero-shot NER) drop-in con LLM
Funciones nuevas en python/functions/datascience/:
- gliner_load_model: carga + cachea modelo GLiNER por (name, device).
  device='auto' resuelve a cuda/cpu segun torch.cuda.is_available, sin
  fallar si torch no esta instalado. ImportError claro si falta gliner.
- extract_entities_gliner: contrato drop-in de extract_entities_llm
  (mismo entity_schema, mismo list[EntityCandidate]). El caller inyecta
  el modelo (cargado UNA vez por proceso). Anota offsets start/end en
  attributes para reconciliar con extract_iocs (issue 0040).

Diferencias vs LLM extractor:
- 50-200x mas rapido en GPU, 0 USD/token.
- Malo con IoCs tecnicos (lo cubre 0037).
- Threshold y flat_ner ajustables por dominio.

pyproject.toml: gliner como extra opcional `[nlp]` para no inflar el
.venv de quien no use NER. Instalacion: `uv pip install -e '.[nlp]'`.

Refs #0038 — Desbloquea 0039 (GLiREL) y 0040 (pipeline hibrido).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:30 +02:00
egutierrez 2098bf42cc docs(issues): cerrar 0037 — IoC regex extractor
- Move dev/issues/0037-ioc-regex-extractor.md a completed/
- Update README link y estado a completado
- Limpiar duplicado obsoleto de 0042 (ya estaba en completed/)

Closes #0037

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:30 +02:00
egutierrez f938371f6e test(cybersecurity): corpus para los 8 extractores + pipeline extract_iocs
30 tests cubriendo positivos y negativos por tipo:
- IPv4 valida/invalida + rangos limite
- IPv6 forma completa/comprimida
- Emails (caracteres validos en local part)
- Dominios con TLD valido vs desconocido
- Hashes MD5/SHA1/SHA256/SHA512 por longitud
- Wallets BTC legacy/bech32 y ETH
- CVEs 4 y 7 digitos
- MAC con `:` y `-` (separadores mezclados rechazados)
- Telefonos E.164 y ES local 9 digitos
- Pipeline filtrado por types y deduplicacion de spans contenidos

Refs #0037

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:30 +02:00
egutierrez cce7764510 feat(cybersecurity): 8 IoC regex extractors + extract_iocs pipeline puro
Extractores nuevos en python/functions/cybersecurity/:
- extract_ip_addresses (IPv4 + IPv6 con validacion ipaddress)
- extract_emails (RFC 5322 simplificado)
- extract_domains (FQDNs con TLD valido, lista estatica)
- extract_file_hashes (MD5/SHA1/SHA256/SHA512, algoritmo por longitud)
- extract_crypto_wallets (BTC legacy + bech32, ETH 0x+40hex)
- extract_cve_ids (CVE-YYYY-NNNN+)
- extract_mac_addresses (xx:xx:xx + xx-xx-xx, separador uniforme)
- extract_phone_numbers (E.164 + ES local 9 digitos)

Pipeline:
- extract_iocs corre todos, deduplica spans contenidos. Mantiene
  purity:pure (kind:function con uses_functions no vacio) porque la
  regla del registry exige que los pipelines sean impuros.

Todas devuelven list[dict] con value/start/end/type para que el
caller (issues 0038-0040) pueda reconciliar offsets con spans NER
sin reparsing.

Refs #0037

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:30 +02:00
egutierrez 7f598e17a1 docs(issues): cerrar 0039 — GLiREL relation extractor
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:18 +02:00
egutierrez 3bf2ed6a5b test(datascience): corpus stub para glirel_load_model + extract_relations_glirel
17 casos: helpers de tokenizacion/mapeo, schema basico con head_pos/tail_pos,
fallback por head_text, threshold, max_pairs, self-loops, ImportError, cache,
device='auto'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:14 +02:00
egutierrez 09f7f0ba1c feat(datascience): GLiREL relation extractor (zero-shot triplets) drop-in con LLM
- glirel_load_model: cache por (model_name, device); device='auto' resuelve via torch
- extract_relations_glirel: tokeniza por whitespace, mapea spans char->token,
  llama predict_relations y devuelve RelationCandidate; fallback text.find si la
  entidad llega sin offsets; max_pairs=N -> top-N por score
- pyproject.toml: glirel en extra nlp

Closes #0039

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:41:09 +02:00
egutierrez 1bd315ce7b docs(issues): cerrar 0038 — GLiNER entity extractor
- Move dev/issues/0038-gliner-entity-extractor.md a completed/
- Update README link y estado a completado

Closes #0038

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:33:53 +02:00
egutierrez b15332686a test(datascience): corpus stub para gliner_load_model + extract_entities_gliner
11 tests sin necesidad de descargar el modelo (200 MB):
- StubModel duck-typed que valida el contrato de predict_entities
- Threshold y flat_ner se propagan al modelo
- Schema vacio lanza ValueError; schema sin labels validos warning + []
- Excepcion del modelo se captura
- Label desconocido se descarta
- gliner_load_model: ImportError simulado, cache hit, _resolve_device
  auto cae a cpu si torch no esta presente

Refs #0038

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:33:46 +02:00
egutierrez c663f9d6e8 feat(datascience): GLiNER entity extractor (zero-shot NER) drop-in con LLM
Funciones nuevas en python/functions/datascience/:
- gliner_load_model: carga + cachea modelo GLiNER por (name, device).
  device='auto' resuelve a cuda/cpu segun torch.cuda.is_available, sin
  fallar si torch no esta instalado. ImportError claro si falta gliner.
- extract_entities_gliner: contrato drop-in de extract_entities_llm
  (mismo entity_schema, mismo list[EntityCandidate]). El caller inyecta
  el modelo (cargado UNA vez por proceso). Anota offsets start/end en
  attributes para reconciliar con extract_iocs (issue 0040).

Diferencias vs LLM extractor:
- 50-200x mas rapido en GPU, 0 USD/token.
- Malo con IoCs tecnicos (lo cubre 0037).
- Threshold y flat_ner ajustables por dominio.

pyproject.toml: gliner como extra opcional `[nlp]` para no inflar el
.venv de quien no use NER. Instalacion: `uv pip install -e '.[nlp]'`.

Refs #0038 — Desbloquea 0039 (GLiREL) y 0040 (pipeline hibrido).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:33:38 +02:00
egutierrez 2cbf754620 docs(issues): cerrar 0037 — IoC regex extractor
- Move dev/issues/0037-ioc-regex-extractor.md a completed/
- Update README link y estado a completado
- Limpiar duplicado obsoleto de 0042 (ya estaba en completed/)

Closes #0037

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:24:25 +02:00
egutierrez 2b82b4b9ce test(cybersecurity): corpus para los 8 extractores + pipeline extract_iocs
30 tests cubriendo positivos y negativos por tipo:
- IPv4 valida/invalida + rangos limite
- IPv6 forma completa/comprimida
- Emails (caracteres validos en local part)
- Dominios con TLD valido vs desconocido
- Hashes MD5/SHA1/SHA256/SHA512 por longitud
- Wallets BTC legacy/bech32 y ETH
- CVEs 4 y 7 digitos
- MAC con `:` y `-` (separadores mezclados rechazados)
- Telefonos E.164 y ES local 9 digitos
- Pipeline filtrado por types y deduplicacion de spans contenidos

Refs #0037

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:24:18 +02:00
egutierrez 55dcdd1164 feat(cybersecurity): 8 IoC regex extractors + extract_iocs pipeline puro
Extractores nuevos en python/functions/cybersecurity/:
- extract_ip_addresses (IPv4 + IPv6 con validacion ipaddress)
- extract_emails (RFC 5322 simplificado)
- extract_domains (FQDNs con TLD valido, lista estatica)
- extract_file_hashes (MD5/SHA1/SHA256/SHA512, algoritmo por longitud)
- extract_crypto_wallets (BTC legacy + bech32, ETH 0x+40hex)
- extract_cve_ids (CVE-YYYY-NNNN+)
- extract_mac_addresses (xx:xx:xx + xx-xx-xx, separador uniforme)
- extract_phone_numbers (E.164 + ES local 9 digitos)

Pipeline:
- extract_iocs corre todos, deduplica spans contenidos. Mantiene
  purity:pure (kind:function con uses_functions no vacio) porque la
  regla del registry exige que los pipelines sean impuros.

Todas devuelven list[dict] con value/start/end/type para que el
caller (issues 0038-0040) pueda reconciliar offsets con spans NER
sin reparsing.

Refs #0037

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:24:11 +02:00
egutierrez 60382db192 chore: add /compile slash command
Compila la app actual (cpp/apps/<X>/ o projects/*/apps/<X>/) para Windows
via MinGW y la copia al escritorio: /mnt/c/Users/lucas/Desktop/apps/<app>/.
Detecta target Android si aparece (hoy ninguna app la tiene).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:34:35 +02:00
egutierrez f2ecc5e14a merge: issue/0049k-graph-explorer-app — graph_explorer + close 0049
Cierra el meta-issue 0049 (OSINT graph viewer + GPU graph rendering system).
Activa feature flag osint_graph_v1.
2026-04-30 00:14:35 +02:00
egutierrez decc468531 feat(0049k): graph_explorer wiring + close issue 0049
- cpp/CMakeLists.txt: register projects/osint_graph/apps/graph_explorer/
  via add_subdirectory pattern (igual que registry_dashboard).
- dev/feature_flags.json: osint_graph_v1 = true (enabled_at 2026-04-30).
- dev/issues/{0049,0049k} → dev/issues/completed/. README index actualizado.

La app vive en su sub-repo dataforge/graph_explorer (push hecho al cerrar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:14:31 +02:00
egutierrez f9be7aad58 merge: issue/0049j-graph-labels — graph_labels + LabelPolicy + ImDrawList overlay 2026-04-29 23:53:35 +02:00
egutierrez 4b28ef6774 feat(viz): graph_labels render con LabelPolicy + ImDrawList (issue 0049j)
graph_labels_draw pinta etiquetas de nodos sobre el FBO del graph_renderer
via ImDrawList. Politica configurable: always-on para selected/hovered/
pinned, top-N por size*(degree+1), culling por viewport AABB y
min_node_pixel_size. Cap duro = max_visible + |always_*|.

API:
- graph_labels_draw(graph, viewport_state, policy, cb, user)
- graph_labels_draw_at(...)  — variante con rect explicito
- graph_labels_select(...)   — helper puro testeable
- graph_compute_degrees(...) — O(E)

Splitting en dos TUs:
- graph_labels.cpp          — funciones draw (depende de ImGui)
- graph_labels_select.cpp   — helpers puros para tests sin ImGui

12 tests en test_graph_labels (culling, max_visible cap, min_pixel_size,
always_* gating por viewport, top-N por score, edge cases). Todos verdes.

Integrado en demos_graph con UI: toggle Labels, sliders Max visible /
Font / Min px, checkboxes Selected/Hovered/Pinned. Golden de
graph_viewport regenerado.

Cierra issue 0049j.
2026-04-29 23:53:32 +02:00
egutierrez 000e9f2ea5 merge: issue/0049i-graph-layouts-static — graph_layouts + viewport multi-select+lasso 2026-04-29 23:42:44 +02:00
egutierrez 4a0750445c feat(viz): graph_layouts (radial/hierarchical/fixed) + viewport multi-select+lasso (issue 0049i)
Phase 1 — graph_layouts:
- New module cpp/functions/viz/graph_layouts.{h,cpp,md} v1.0.0
- layout_grid, layout_circular, layout_random (migrated from graph_force_layout.cpp)
- layout_radial: BFS rings from root, hop k -> circle of radius k*ring_spacing
- layout_hierarchical: Sugiyama-style heuristic (longest-path levels + barycenter ordering)
- layout_fixed: no-op
- All respect NF_PINNED. graph_layout_circular/grid kept as deprecated wrappers.

Phase 2-3 — graph_viewport v1.2.0:
- Multi-selection via state.selection (vector<int>); NF_SELECTED kept in sync
- Lasso: Shift+Drag on empty area; AABB hit-test on release
- Drag of N-selection: all selected pinned + moved by mouse delta
- Ctrl+click toggle, Esc clears selection
- Right-click on node -> on_context_menu callback
- Double-click on node -> on_double_click callback
- Helpers exposed: graph_viewport_clear/add_to/toggle/is_selected (own TU for tests)

Phase 4 — tests:
- test_graph_layouts: 12 cases / 364 assertions covering geometry, pin, edges
- test_graph_viewport: 5 cases for selection helpers (pure logic, no GL)

Phase 5 — demo (primitives_gallery):
- Layout combo (force/grid/circular/radial/hierarchical/fixed) + Apply button
- Right-click popup with Pin/Unpin/Add-to-selection
- Status overlay shows [N selected] when selection non-empty
- Updated golden images

Issue moved to dev/issues/completed/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:42:31 +02:00
egutierrez d09b35b533 merge: issue/0049h-graph-force-layout-gpu — graph_force_layout_gpu compute + spatial hash 2026-04-29 23:29:21 +02:00
egutierrez 9c26c3917c feat(viz): graph_force_layout_gpu compute + spatial hash (issue 0049h)
Layout force-directed en GPU usando 5 compute shaders 4.3 + spatial hash
grid 64x64. API simetrica con graph_force_layout (CPU) para que el consumer
pueda swappear sin cambios. atomicCompSwap loop para float-add portable.

- cpp/functions/viz/graph_force_layout_gpu.{h,cpp,md}: nuevo modulo
- cpp/functions/gfx/gl_loader: anade glDispatchCompute, glMemoryBarrier,
  glBindBufferBase, glGetBufferSubData (Windows wgl)
- cpp/tests/test_graph_force_layout_gpu.cpp: smoke + pinned + CPU vs GPU.
  Crea ventana GLFW oculta GL 4.3; SKIP si headless o sin compute.
- demos_graph: checkbox "GPU layout" para swappear CPU/GPU en runtime
- issue movido a dev/issues/completed/
2026-04-29 23:29:16 +02:00
egutierrez e0f2d919de merge: issue/0049g-graph-source-operations — graph_sources lector operations.db + streaming 2026-04-29 23:12:43 +02:00
egutierrez 474c2822bc feat(viz): graph_sources lector operations.db + streaming (issue 0049g)
- graph_load_from_operations: SQLite read-only, schema-detect (type_ref/type,
  from_entity/source, to_entity/target, name/type, weight, updated_at).
- 16-color indigo palette por hash FNV1a32 del nombre de tipo. user_data
  por nodo es FNV1a64(entity.id) — deterministico entre cargas.
- Label pool interno: metadata.name (JSON simple) > entities.name > id.
- graph_free libera nodes/edges/types/rel_types/labels/strdup'd names via
  arena_map (GraphData* -> arena).
- Streaming pull-based con tiebreak (updated_at, id) y crecimiento x2 de
  capacidad. Tipos nuevos descubiertos en stream se anaden a types.
- Tests: fixture in-memory (3 entity types, 2 rel types, 10 entities,
  15 relations) + smoke contra apps/script_navegador/operations.db.
- Issue movido a completed/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:12:31 +02:00
egutierrez bab12de60d merge: issue/0049f-graph-renderer-symbols — renderer shapes/iconos/flechas/edge-styles 2026-04-29 23:01:54 +02:00
egutierrez c967c2edfd feat(viz): renderer shapes/iconos/flechas/edge-styles (issue 0049f)
graph_renderer 1.5.0:
- 6 shapes SDF (circle, square, diamond, hex, triangle, rounded square)
  con dispatch en fragment shader y AA via fwidth.
- Atlas opcional de iconos Tabler bakeado por graph_icons; el shader
  compone overlay desde un uniform vec4 u_icon_uvs[256]. Setter publico
  graph_renderer_set_icon_atlas(r, tex, uv_table, count).
- Aristas direccionales: 6 vertices por arista (line + chevron de la
  flecha) en una sola draw call; segmento principal acortado por el
  radio del nodo target.
- Edge styles solid/dashed/dotted via descarte por arc_length en el
  fragment shader; las lineas del chevron son siempre solidas.

graph_icons 1.0.0 (nuevo):
- Atlas RGBA8 512x512 = grid 16x16 (256 iconos max) bakeado con
  stb_truetype desde tabler-icons.ttf.
- API: graph_icons_build/texture/region/uv_table/destroy. icon_id es
  1-based; 0 reservado para "sin icono".
- Hook FN_GRAPH_ICONS_SKIP_GL=1 para tests sin contexto GL.

Demo demos_graph_styles en primitives_gallery: 6 EntityTypes (uno por
shape) con icono Tabler representativo + 3 RelationTypes (knows/uses/
owns) con flechas direccionales y los 3 estilos.

test_graph_icons: 6 casos cubriendo bake, regiones 1-indexed, uv_table
consistente, layout en grid 16x16, validacion de count fuera de rango,
y verificacion de alpha != 0 en las celdas tras bake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:01:49 +02:00
egutierrez 34a3addc56 merge: issue/0049e-graph-types-extended — graph_types modelo extendido + EntityType/RelationType + flags 2026-04-29 22:44:44 +02:00
egutierrez c29428a187 feat(viz): graph_types modelo extendido + EntityType/RelationType + flags (issue 0049e)
Extiende el modelo agnostico de graph_types.h para soportar shapes/iconos/
filtros/labels/streaming sin acoplar a backend. Migra el unico consumer
(demos_graph) en el mismo cambio.

- GraphNode v2: type_id + shape_override/color_override/size_override +
  flags (NF_PINNED/VISIBLE/SELECTED/HOVERED) + label_idx + user_data.
- GraphEdge v2: type_id + style_override + flags (EF_DIRECTED/VISIBLE).
- EntityType / RelationType: tablas en GraphData (types, rel_types).
- Helpers de resolucion (resolve_node_color/shape/size, resolve_edge_*)
  y constructores ergonomicos (graph_node, graph_edge, entity_type,
  relation_type) — sentinel-based para herencia automatica del tipo.
- graph_renderer v1.4: lee NF_VISIBLE / EF_VISIBLE, resuelve apariencia
  via override → EntityType → fallback indexado por type_id. Skipea
  aristas con endpoints invisibles. Shapes siguen pintandose como
  circulo (0049f cableara el dispatch real).
- graph_force_layout v1.2: pinned ahora vive en flags & NF_PINNED.
- graph_viewport v1.1: hover/seleccion publican NF_HOVERED/SELECTED en
  el grafo (clear-then-set). Drag usa NF_PINNED. Tooltip muestra Type/
  user_data en lugar de community/value/label.
- demos_graph: 8 EntityType (paleta antigua) + 1 RelationType. type_id
  por cluster. user_data = indice numerico del nodo. Apariencia visual
  identica al pre-cambio.
- test_graph_types.cpp: 12 casos cubriendo helpers, defaults, bitmask
  manipulation y resoluciones override-vs-EntityType. test_graph_edge_
  static actualizado al nuevo modelo (ya no tiene .color directo).
- 4 .md de tipos nuevos (graph_node, graph_edge, entity_type,
  relation_type) + GraphData v2.0 actualizado.

Tests: 31/31 ctest verdes (incluye test_visual golden).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:44:40 +02:00
egutierrez ae47b76d0c merge: issue/0049d-graph-edges-vertex-pulling — graph_renderer aristas via vertex pulling + TBO 2026-04-29 22:33:00 +02:00
egutierrez daf491cd99 perf(viz): graph_renderer edges via TBO + vertex pulling (issue 0049d)
El buffer de aristas pasa a estatico (16B/arista: source, target, color,
flags) y solo se reupload cuando cambia el grafo. Las posiciones de los
nodos viven en un Texture Buffer Object (RG32F) actualizado por frame; el
vertex shader hace texelFetch con gl_VertexID & 1 para elegir endpoint.
Draw call: glDrawArraysInstanced(GL_LINES, 0, 2, edge_count) con divisor=1.

Para 100k aristas: el upload de 4.8 MB/frame baja a 0 en regimen estable.
edge_alpha pasa a uniform; la pre-multiplicacion en CPU desaparece. GLSL
sigue en 330 core (samplerBuffer/texelFetch estan en 1.40+).

gl_loader gana glBufferSubData, glVertexAttribIPointer y glTexBuffer (en
Linux ya estaban via GL_GLEXT_PROTOTYPES; ahora estan disponibles tambien
en MinGW/Windows).

Tests: nuevo test_graph_edge_static valida el layout de 16B y el packing
RGBA8 del fallback. test_visual sigue verde — render visualmente identico.

Bump graph_renderer 1.2.0 -> 1.3.0.
2026-04-29 22:32:38 +02:00
egutierrez b156942cea merge: issue/0049c-graph-renderer-tier1 — RGBA8 + orphan + frustum cull + auto-pause helper 2026-04-29 22:17:22 +02:00
egutierrez 02b4141cc1 perf(viz): graph_renderer Tier 1 (RGBA8 + orphan + frustum cull) + force_layout auto-pause helper
Issue 0049c. Tres optimizaciones internas en graph_renderer.cpp + un
helper puro en graph_force_layout para detectar convergencia. API publica
intacta — solo cambian el layout interno de los buffers, el shader y
los costes por frame.

1. RGBA8 color packing
   - El instance buffer de nodos pasa de (x,y,size,r,g,b,a) 28B a
     (x,y,size,color_u32) 16B (-43%). Aristas: 24B → 12B/vertex (-50%).
   - Shaders desempaquetan con bit shifts (compatible GL 3.30+, no
     necesita unpackUnorm4x8 que es 4.20+).
   - Helpers expuestos: pack_rgba8 / unpack_rgba8 / modulate_alpha_rgba8
     en graph_renderer.h. Los GraphNode.color y la paleta ya tenian el
     layout correcto (R en LSB), asi que CPU ahora pasa el uint32 directo
     sin convertir a 4 floats por nodo y por frame.

2. Capacity-tracked streaming buffers
   - Sustituye el doble glBufferData de antes por:
       glBufferData(NULL, capacity, STREAM_DRAW)   // orphan + reserva
       glBufferSubData(0, used_bytes, data)        // solo lo usado
   - capacity crece x2 cuando hace falta (inicial 4096 nodos /
     8192 vertices de aristas) → reallocaciones en O(log N).
   - Staging CPU (NodeInstance* / EdgeVertex*) reusado entre frames con
     realloc, no malloc/free per frame.

3. Frustum cull (CPU-side)
   - AABB del viewport en world coords con margen 10%.
   - Aristas: skip si AABB del segmento no intersecta el viewport.
   - Nodos: solo los visibles entran al instance buffer; visible_count
     es el N que pasa a glDrawArraysInstanced. Pop-in de borde mitigado
     por el margen.

4. graph_force_layout_should_pause(low_frames, min_consecutive)
   - Helper puro: el caller mantiene el contador, la funcion solo
     decide si parar. Reemplaza la rama inline en demos_graph.cpp.
   - Test Catch2 con secuencias artificiales.

Tests: test_graph_pack_rgba8 (16401 asserts, 4 cases — roundtrip exhaustivo
+ alpha modulation + clamp). test_graph_should_pause (3 cases, 14 asserts).
Los 29 tests del cpp/tests/ siguen verdes (incluido test_visual con goldens).

Bump versiones:
- graph_renderer 1.1.0 → 1.2.0
- graph_force_layout 1.0.0 → 1.1.0  (tested: true via should_pause test)
2026-04-29 22:17:13 +02:00
egutierrez 0e6a013937 feat(graph): wheel-zoom no scrollea, slider 1M nodos, edges/node configurable
Tres mejoras de UX/escala en el demo de grafos:

1. **Wheel zoom dentro del canvas no scrollea la pagina**
   En graph_viewport.cpp tras procesar MouseWheel para zoom hacemos
   io.MouseWheel = 0 — consume el evento para que el BeginChild padre
   (la galeria) no scrollee a la vez que el grafo se acerca. Antes
   sentia "doble accion" al rodar la rueda sobre el canvas.

2. **graph_force_layout: pool dinamico (soporta 1M nodos)**
   El array static QuadNode[1<<20] (~48MB siempre reservados, tope
   rigido en ~250k nodos por la fan-out) se reemplaza por
   std::vector<QuadNode>. graph_force_layout_step llama a
   quad_pool_reserve(5*N + 1024) ANTES de construir el arbol — asi las
   referencias QuadNode& que mantenemos vivas durante quad_subdivide
   no se invalidan por reallocaciones a mitad del build (resize solo
   ocurre en el reserve inicial). Memoria escala lineal con N: 1M
   nodos ≈ 240MB de pool, una vez por programa.

3. **Demo de grafo: sliders extendidos + cluster_r escala con sqrt(N)**
   - "Nodes" pasa de 100..20k a 100..1M con escala logaritmica
     (ImGuiSliderFlags_Logarithmic) para que el rango medio sea util.
   - Nuevos sliders "Edges/node" (1..10) e "Inter %" (0..30%) — antes
     hardcoded a 3 y 5%.
   - cluster_radius y scatter ahora escalan con sqrt(N): a 1k nodos
     ~370 px de radio, a 1M ~12000 px. Antes era constante a 200/40
     y los nodos quedaban empaquetados al subir N — visualmente "sin
     limite cuadrado", esparcidos sobre un area proporcional al grafo.
   - Golden de graph_viewport regenerado por la nueva fila de sliders.

Notas:
- A 1M nodos sin GPU compute esta limitado por el upload de aristas
  (vertex pulling con TBO llega en 0049d). Render mantenible hasta
  ~200-300k.
- En Linux/Windows ambos builds limpios. 27/27 tests verde.
2026-04-29 21:53:33 +02:00
egutierrez 9a4ff33e68 perf(graph): quick wins — OpenMP force step + buffer orphan + auto-pause
Tres atajos de rendimiento sin GPU compute (eso llega en 0049h). Probados
en Linux y cross-compile Windows, todos los tests pasan, OpenMP 4.5
detectado.

1. **OpenMP en graph_force_layout_step** (cpp/functions/viz/...)
   - find_package(OpenMP) en cpp/CMakeLists.txt; fn_framework lo enlaza
     PUBLIC para que cualquier app/funcion lo herede transparentemente.
     Si no esta disponible, los pragmas se ignoran (single-thread).
   - #pragma omp parallel for con guard if(N>=1024) en los 4 bucles
     embarazosamente paralelos: zero forces, repulsion Barnes-Hut (con
     schedule dynamic), gravity, integration (con reduction sobre energy).
     La attraction-along-edges se queda secuencial: edges multiples
     escriben en el mismo nodo y meterle atomic mata el speedup.
   - quad_force usaba un static int stack[1<<20] (4MB compartidos entre
     threads — race). Lo reemplazo por int stack[256] en pila: el
     quadtree crece como log4(N) ~= 10 niveles para N <= 1M, asi que 256
     es holgado y thread-safe sin coste.
   - Esperable: ~4-8x menos tiempo CPU/step en 20k nodos en CPU multicore.

2. **Buffer orphan en graph_renderer** (edges + nodes)
   - Antes del glBufferData(.., data, DYNAMIC_DRAW), un primer
     glBufferData(.., NULL, DYNAMIC_DRAW) que descarta el buffer previo.
     El driver da uno fresco sin esperar al frame anterior — evita los
     sync stalls clasicos del DYNAMIC_DRAW reuploadeado cada frame.
   - Esperable: 2-3x throughput de upload (Mesa/NVIDIA/AMD respetan el
     hint).

3. **Auto-pause en demo_graph cuando converge**
   - Si energy_per_node < 0.001 durante 30 frames consecutivos, paramos
     la simulacion automaticamente. CPU/GPU a 0% cuando el grafo ya
     esta estable. Resume con "Resume layout" o "Regenerate".

Lo de OpenMP se sustituye cuando entre 0049h (force layout en compute
shader): cuando llegue, los #pragma omp se borran. Orphan y auto-pause
son keepers definitivos.
2026-04-29 21:38:13 +02:00
egutierrez 96db47f083 fix(primitives_gallery): preserve scroll position when font size changes
Cuando se cambia "Size" en Settings la fuente se escala via
style.FontSizeBase y el contenido del child "##gallery_content" crece o
encoge proporcionalmente. La scroll_y se quedaba en pixeles absolutos,
asi que la linea logica visible "se bajaba" al usuario tras el cambio
de zoom.

Fix: cachear FontSizeBase entre frames y, cuando cambia, escalar
scroll_y por el ratio nuevo/viejo. Mantiene la misma linea arriba del
viewport — sin saltos.
2026-04-29 21:32:44 +02:00
egutierrez 492e6b59cd feat(framework): bump OpenGL 3.3 → 4.3 core context
Cierra 0049b. El context de fn::run_app pide ahora GL 4.3 core con
forward-compat global, habilitando compute shaders, SSBOs, image
load/store y atomic counters — bloques esenciales del graph_renderer GPU
del proyecto osint_graph (issues 0049f y 0049h).

Cambios:

- cpp/framework/app_base.cpp: 4.3 core + forward-compat. Comentario
  marcando que es backward-compatible con shaders #version 330.
- cpp/apps/primitives_gallery/capture.cpp: deja explicitamente 3.3 core
  porque WSL Mesa no entrega 4.3 offscreen (GLXBadFBConfig); ImGui +
  ImPlot funcionan igual en 3.3 para los goldens.
- primitives_gallery: nuevo demo Gfx > gl_info que muestra
  Vendor/Renderer/Version/GLSL en runtime + status 4.3 (verde) +
  limites (MAX_TEXTURE_SIZE, MAX_VERTEX_ATTRIBS, MAX_UNIFORM_BLOCK_SIZE
  y, si 4.3+, MAX_SHADER_STORAGE_BUFFER_BINDINGS y compute shared mem).
  Solo glGetString/glGetIntegerv — sin loader extra.
- About bumped a 0.4.0 con la nota del nuevo demo y de GL 4.3.
- cpp/tests/test_visual.cpp: usa LIBGL_ALWAYS_SOFTWARE=1 al lanzar el
  capture para alinear el driver con update_goldens.sh; sin esto las
  diferencias de strings (llvmpipe vs d3d12) hacen que gl_info supere
  el 1% de tolerancia.
- cpp/tests/golden/gl_info.png: nuevo golden.

Build verificado en Linux (cmake build OK) + Windows cross-compile
(cmake build OK). Las 27 pruebas pasan (incluida test_visual con 42
demos comparadas).
2026-04-29 21:23:15 +02:00
egutierrez 4f25564132 feat(projects): osint_graph project + graph_explorer sub-repo bootstrap
Cierra 0049a. Estructura local en projects/osint_graph/ (gitignored):

- project.md con frontmatter (name, description, tags).
- vaults/vault.yaml + symlink osint_data → ~/vaults/osint_graph/{raw,
  processed,exports}.
- apps/graph_explorer/ inicializado como sub-repo Gitea
  (dataforge/graph_explorer, branch master) con commit vacio.

Indexado verificado: 4 projects, 2 vaults; fn show osint_graph OK.
2026-04-29 21:08:47 +02:00
egutierrez f8f72e4bf7 chore(issues): plan 0049 OSINT graph viewer multi-issue
Aggregates the planning artifacts for the 0049 series (umbrella + 0049a..0049k):

- New rule cpp_apps.md (registered in INDEX) — standardize structure, CMake
  patterns, app.md frontmatter and sub-repo for C++ apps; points to the
  authoritative cpp/PATTERNS.md and cpp/DESIGN_SYSTEM.md.
- Feature flag osint_graph_v1 (disabled until 0049k closes).
- Issue 0049 (umbrella) and sub-issues 0049b..0049k describing the GPU
  rendering system, force-layout, types, sources, labels and the final
  graph_explorer app integration.
- README updated with the new rows (all pending; 0049a will flip to
  completed in the next commit).
2026-04-29 21:08:36 +02:00
egutierrez 4cdd650502 feat(cpp/apps): bump versions — chart_demo 0.2, gallery 0.3, shaders_lab 0.3 2026-04-29 00:56:24 +02:00
egutierrez b7d44c0347 feat(cpp/framework): viewports=true por defecto en AppConfig — ventanas arrastrables fuera del main 2026-04-29 00:54:43 +02:00
egutierrez d7dab5eec5 fix(primitives_gallery): Windows mkdir() solo acepta el path en --capture 2026-04-29 00:31:53 +02:00
egutierrez 5851ac97cd docs(issues): cerrar 0046 — actualizar README 2026-04-29 00:31:10 +02:00
egutierrez c686780377 merge: issue/0046-cpp-refactor-raw-imgui — implementación paralela 2026-04-29 00:30:36 +02:00
egutierrez d3cf6d2774 docs: cerrar issue 0046 2026-04-29 00:29:54 +02:00
egutierrez c0e5bc711b refactor(shaders_lab): usar modal_dialog en save-as (issue 0046)
El modal Save-as-generator usaba BeginPopupModal + InputText + Button
crudo. Ahora usa fn_ui::modal_dialog_begin/end + fn_ui::text_input +
fn_ui::button del registry. El error inline usa fn_tokens::colors::error
en vez de ImVec4(1, 0.4, 0.4, 1). Anade modal_dialog.cpp, text_input.cpp
y button.cpp al CMakeLists del app.

Raw ImGui::Begin*/Selectable/BeginPopupModal: 11 -> 8.
2026-04-29 00:29:50 +02:00
egutierrez eae1829923 refactor(primitives_gallery): usar tree_view en sidebar (issue 0046)
El sidebar agrupaba demos por categoria con un Selectable+PushStyleColor
manual por item. Ahora usa fn_ui::tree_view con las categorias como
ramas (default-open via SetNextItemOpen + ImGuiCond_FirstUseEver) y las
demos como hojas seleccionables. Visualmente equivalente: separadores
por categoria, item activo coloreado.

Raw ImGui::Begin*/Selectable: 4 -> 3 (Selectable eliminado).
2026-04-29 00:29:44 +02:00
egutierrez 32927c5e9f docs(issues): cerrar 0048 — actualizar README 2026-04-29 00:20:13 +02:00
egutierrez 61ae9fde4b merge: issue/0048-cpp-visual-tests-ci-gate — implementación paralela 2026-04-29 00:19:37 +02:00
egutierrez 2ba71c6d79 docs: cerrar issue 0048 2026-04-29 00:18:58 +02:00
egutierrez 0fc30ae4dd docs(cpp): tests visuales y CI gate en PATTERNS.md
Nueva seccion "Tests visuales y CI gate (issue 0048)" describiendo:
- Como capturar/regenerar goldens con cpp/scripts/update_goldens.sh.
- Como diagnosticar un diff (PNG actual en cpp/build/tests/visual_actual/
  vs golden en cpp/tests/golden/).
- Cuando test_visual SKIPea (sin goldens, sin binario, sin GL).
- CI gate check_tested.sh y los pasos para satisfacerlo.

Issue 0048.
2026-04-29 00:18:51 +02:00
egutierrez 04d89ec8ae chore(cpp/scripts): update_goldens.sh y check_tested.sh
- update_goldens.sh: build primitives_gallery + lanza --capture sobre
  cpp/tests/golden/ con LIBGL_ALWAYS_SOFTWARE=1.
- check_tested.sh [days]: CI gate que falla si una funcion C++ creada en
  los ultimos N dias (default 30) no tiene tested:true en su .md. Hookeado
  al final de run_tests.sh. No-op si registry.db no existe.

Issue 0048.
2026-04-29 00:18:45 +02:00
egutierrez e750894847 feat(cpp/tests): test_visual con png diff vs goldens (skip si vacio)
- png_diff.{h,cpp}: pixel_diff_ratio(path_a, path_b, channel_threshold) con
  stb_image. Devuelve PngDiffResult con pixels_total, pixels_different y
  diff_ratio. Si dimensiones difieren, diff_ratio=1.0.
- test_visual.cpp: invoca primitives_gallery --capture sobre tmpdir, compara
  cada PNG vs cpp/tests/golden/<demo>.png con tolerancia 1% pixels distintos
  (threshold 5/255 por canal). SKIPea con WARN si:
  * golden dir vacio (no hay goldens todavia)
  * binario primitives_gallery no construido
  * el binario falla al capturar (entorno sin GL)
- CMakeLists: registra test_visual con FN_TEST_GOLDEN_DIR, FN_TEST_GALLERY_BIN,
  FN_TEST_TMP_DIR y FN_TEST_REPO_ROOT (para que la captura corra desde la
  raiz del repo y resuelva paths relativos como sql_workbench's registry.db).
- golden/: 41 PNGs iniciales generados en este entorno (WSL +
  LIBGL_ALWAYS_SOFTWARE=1). Pueden regenerarse con cpp/scripts/update_goldens.sh.

Issue 0048.
2026-04-29 00:18:39 +02:00
egutierrez 6be660fac6 feat(primitives_gallery): añadir --capture <dir> mode (offscreen render + glReadPixels)
Modo de captura que renderiza cada demo de la gallery en una ventana GLFW
invisible (GLFW_VISIBLE=GLFW_FALSE) y guarda PNG por demo via stb_image_write.

- capture.{h,cpp}: API gallery::run_capture(cfg, items) — warmup_frames,
  glReadPixels(GL_RGBA), flip vertical, stbi_write_png.
- main.cpp: parsea --capture <dir> antes de fn::run_app y delega a capture.cpp.
- vendor: stb_image_write.h v1.16 (mismo commit que stb_image.h).

Funciona en WSL con LIBGL_ALWAYS_SOFTWARE=1 (Mesa/llvmpipe). Si el entorno
no tiene contexto GL, el binario sale con rc!=0 sin generar PNGs.

Issue 0048.
2026-04-29 00:18:27 +02:00
egutierrez 98e134c935 docs(issues): cerrar 0043 — actualizar README 2026-04-29 00:10:25 +02:00
egutierrez cf8c602291 merge: issue/0043-cpp-apps-standardize-shell — implementación paralela 2026-04-29 00:09:43 +02:00
egutierrez ab3f1d442a docs: cerrar issue 0043 2026-04-29 00:08:56 +02:00
egutierrez 690b01e169 refactor(shaders_lab): usar AppConfig.panels + layouts_cb (issue 0043) 2026-04-29 00:08:40 +02:00
egutierrez 1b5d05dbaf refactor(primitives_gallery): usar AppConfig.about + init_gl_loader (issue 0043) 2026-04-29 00:06:51 +02:00
egutierrez 6bdcc98911 refactor(chart_demo): usar AppConfig.about (issue 0043) 2026-04-29 00:06:20 +02:00
egutierrez 98b7b460b8 docs(issues): cerrar 0045 — actualizar README 2026-04-29 00:00:56 +02:00
egutierrez 6286fdb9c7 merge: issue/0045-cpp-extract-pure-logic — implementación paralela 2026-04-28 23:59:45 +02:00
egutierrez 5a25095a54 docs: cerrar issue 0045 2026-04-28 23:59:08 +02:00
egutierrez 4f280f34d7 test(cpp): tests para sql_parse, process_state_machine, file_poll_diff 2026-04-28 23:58:40 +02:00
egutierrez 0cfaa27ee1 refactor(shaders_lab): extraer compile_* a compiler.{h,cpp} 2026-04-28 23:56:33 +02:00
egutierrez f2521bd7d2 refactor(cpp/core): file_watcher usa file_poll_diff 2026-04-28 23:55:11 +02:00
egutierrez 02c9dd93e3 feat(cpp/core): añadir file_poll_diff pure 2026-04-28 23:53:49 +02:00
egutierrez 90e9593b20 refactor(cpp/core): process_runner usa process_state_machine 2026-04-28 23:53:08 +02:00
egutierrez 86a1e12204 feat(cpp/core): añadir process_state_machine pure 2026-04-28 23:52:37 +02:00
egutierrez 9a543b7502 refactor(cpp/core): sql_workbench usa sql_parse 2026-04-28 23:51:59 +02:00
egutierrez 24cd142814 feat(cpp/core): añadir sql_parse pure 2026-04-28 23:51:23 +02:00
egutierrez 6430e993c6 docs(issues): cerrar 0041, 0042, 0044, 0047 + actualizar README 2026-04-28 23:45:40 +02:00
egutierrez 54d9f1809f merge: issue/0047-cpp-tests-foundation — implementación paralela 2026-04-28 23:44:55 +02:00
egutierrez 0330aaedd0 merge: issue/0044-cpp-orphans-audit — implementación paralela 2026-04-28 23:44:52 +02:00
egutierrez 08e2b1280f merge: issue/0042-cpp-layout-storage-public — implementación paralela 2026-04-28 23:44:44 +02:00
egutierrez eb5e3ef9ab merge: issue/0041-cpp-app-best-practices — implementación paralela 2026-04-28 23:44:24 +02:00
egutierrez 04ccf3f108 chore(cpp): script run_tests.sh para build+ctest one-shot 2026-04-28 23:42:38 +02:00
egutierrez 7c86070cc6 docs(registry): tested:true + test_file_path en .md de primitivos
20 funciones C++ pasan de tested:false a tested:true con sus tests
correspondientes y test_file_path apuntando a cpp/tests/. Cubre 4 tests
reales (tween_curves, pie/kpi/bar math) y 16 placeholders (componentes
UI con tests visuales pendientes para 0048).

Coverage cpp pasa de 4% (3/81) a 28% (23/81).
2026-04-28 23:42:35 +02:00
egutierrez dfd73ec158 test(cpp): placeholders para top-19 primitivos UI (logica visual en 0048)
Cada placeholder garantiza que el .cpp compila y linka contra Catch2,
y reserva el slot para tests futuros una vez se extraiga logica pura
del componente. La validacion visual real vive en primitives_gallery
(issue 0048).

Cubre: tokens, button, select, text_input, badge, kpi_card, pie_chart,
bar_chart, tree_view, modal_dialog, toolbar, toast, empty_state,
page_header, dashboard_panel, dashboard_grid, sparkline, table_view,
icon_button.
2026-04-28 23:42:29 +02:00
egutierrez 0779c34ca8 test(cpp): tests reales para tween_curves, pie/kpi/bar math
- test_tween_curves: boundary conditions (t=0, t=0.5, t=1), monotonicidad
  para curvas no oscilantes, dispatch via apply(), names() no nulos.
- test_pie_chart_math: replica slice_at (anonymous namespace en pie_chart.cpp)
  y testea hit-test angular, edge del radio, distribucion proporcional.
- test_kpi_card_math: classify_delta (Up/Down/Flat) y pct_change con
  zero/negativos. La logica visual la cubre primitives_gallery (issue 0048).
- test_bar_chart_math: compute_y_range (incluye 0, negativos, vacio,
  single value) y clamp_bar_width [0.05, 1.0].
2026-04-28 23:42:22 +02:00
egutierrez 93a964a47d feat(cpp): integrar Catch2 en CMake con BUILD_TESTING + add_fn_test helper
cpp/tests/CMakeLists.txt compila Catch2 amalgamated como STATIC libreria
una sola vez. Cada test es su propio executable (CATCH_CONFIG_MAIN por
archivo) y se registra con add_test(). add_fn_test(name srcs...) es el
helper: incluye paths de cpp/functions y cpp/framework, linka catch2.

Tests que necesitan symbols reales (fn_framework, imgui) los anaden
explicitamente con target_link_libraries despues.
2026-04-28 23:42:14 +02:00
egutierrez 0585f851bb feat(cpp): vendor Catch2 v3.5.0 amalgamated
BSL-1.0. Single-header + single-source para tests con ctest.
2026-04-28 23:42:08 +02:00
egutierrez cdba3869b2 docs: cerrar issue 0042 2026-04-28 23:41:19 +02:00
egutierrez 298bb27685 docs(rules): registrar uses_functions en INDEX
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:41:17 +02:00
egutierrez 7dc5b51726 refactor(shaders_lab): migrar layouts inline a layout_storage publico
Sustituye ~30 lineas de cableado manual de save/load/list/delete contra
layout_storage_sqlite por dos llamadas a la nueva API publica:

    g_layouts = fn_ui::layout_storage_open("shaders_lab.db");
    fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb);

El blob pendiente lo gestiona el storage (layout_storage_apply_pending).
on_reset se override para ademas re-mostrar los paneles de shaders_lab.
La tabla ui_layouts heredada queda intacta — la nueva API usa
imgui_layouts en la misma BD.
2026-04-28 23:41:03 +02:00
egutierrez 10599d23af docs(rules): añadir regla uses_functions
Documenta la convencion de uses_functions para C++:
- El indexer no deduce automaticamente las dependencias C++
- El .md del consumidor declara las dependencias
- Framework (cpp/framework/) y apps (cpp/apps/) no se registran en
  uses_functions; se anotan en notes: del huerfano

Tambien indexada en .claude/rules/INDEX.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:40:58 +02:00
egutierrez 5b73466bd4 chore(registry): notes en huerfanas usadas por framework/apps
Auditoria del issue 0044: anota en notes: el contexto de consumo de
huerfanos que no pueden registrarse en uses_functions porque sus
consumidores no son funciones del registry:
- consumido por cpp/framework/app_base.cpp (framework no indexado)
- consumido por cpp/apps/{shaders_lab,chart_demo,text_editor_smoke}/main.cpp
- scaffolding/demo en primitives_gallery

31 huerfanas anotadas. Las que quedan en uses_functions=[] tras esto
son hojas legitimas (no llaman a nada) o realmente sin uso (lista
DEAD reportada en el issue 0044).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:40:51 +02:00
egutierrez 7c95b36a5f chore(registry): añadir uses_functions a consumidores reales (viz)
Auditoria del issue 0044: 9 archivos .md de cpp/functions/viz/ con
uses_functions actualizado. Resuelve dependencias detectadas via
#include: plot_static (consumido por bar_chart, histogram, line_plot,
pie_chart, scatter_plot), gl_loader, gl_framebuffer, gl_shader,
graph_force_layout, graph_renderer, graph_spatial_hash, orbit_camera,
sparkline y tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:40:37 +02:00
egutierrez 37ca9562c3 chore(registry): añadir uses_functions a consumidores reales (gfx)
Auditoria del issue 0044: 14 archivos .md de cpp/functions/gfx/ con
uses_functions actualizado. Resuelve dependencias detectadas via
#include: gl_loader (consumido por casi todo el dominio gfx),
dag_catalog (consumido por la familia dag_*), fullscreen_quad,
gl_framebuffer, gl_shader, mesh_obj_load, uniform_parser y
dag_node_previews.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:40:31 +02:00
egutierrez ee3c39ad25 chore(registry): añadir uses_functions a consumidores reales (core)
Auditoria del issue 0044: 17 archivos .md de cpp/functions/core/ con
uses_functions actualizado para reflejar las llamadas reales detectadas
mediante #include en sus .cpp/.h. Los huerfanos referenciados (tokens,
app_about, app_settings, layouts_menu, panel_menu, table_view,
text_editor, tween_curves, app_settings) ahora aparecen en el grafo de
dependencias del registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:40:22 +02:00
egutierrez c659120f86 feat(cpp/core): añadir layout_storage publico (SQLite-backed LayoutCallbacks)
API publica con handle opaco LayoutStorage* que envuelve la persistencia
de layouts ImGui en SQLite. Cualquier app puede obtener un LayoutCallbacks
listo para app_menubar/layouts_menu_items con dos llamadas:

    auto* st = fn_ui::layout_storage_open("app.db");
    fn_ui::LayoutCallbacks cb;
    fn_ui::layout_storage_make_callbacks(st, cb);

Tabla SQLite imgui_layouts(name, ini, updated_at) creada con
CREATE TABLE IF NOT EXISTS para no chocar con tablas pre-existentes.
fn_framework ahora enlaza SQLite::SQLite3 para que cualquier app que use
el framework herede acceso a layout_storage sin trabajo extra.
2026-04-28 23:39:34 +02:00
egutierrez a0c0aa4f18 docs(issues): añadir 0041-0048 — refactor C++ apps, tests, primitives standarization 2026-04-28 23:38:56 +02:00
egutierrez a99aa661a2 fix(cpp/viz,core): bell icon TI_BELL, candlestick Setup-inside-BeginPlot, pie legend, kpi sparkline a la derecha
- toast.cpp: TI_BELL en lugar de \xf0\x9f\x94\x94 (fuera del rango cargado por icon_font, renderizaba como ?)
- candlestick.cpp: SetupAxes/SetupAxisScale/SetupAxisLimits movidos dentro de BeginPlot/EndPlot — antes el plot desaparecia al entrar
- pie_chart.cpp: SetupLegend(East, Outside, NoButtons), eliminado NoLegend
- kpi_card.cpp: layout 2 cols con sparkline a la derecha centrado verticalmente
2026-04-28 23:38:51 +02:00
egutierrez ecbc471a3a feat(cpp/framework): extender AppConfig con about, panels, layouts_cb, init_gl_loader 2026-04-28 23:37:23 +02:00
egutierrez 082376a46a docs(cpp): añadir PATTERNS.md con checklist de apps 2026-04-28 23:34:07 +02:00
egutierrez 200e98e94c docs(diary): entrada 2026-04-28
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:42:48 +02:00
egutierrez ec81bf360d docs: ADR 0002 + CHANGELOG + reglas para dataforge/<name>+master
- docs/adr/0002-apps-analyses-as-dataforge-master.md: decision arquitectural
  con contexto, alternativas descartadas y cambios concretos del 2026-04-28.
- CHANGELOG.md: entrada 2026-04-28 con Added/Changed/Fixed.
- .claude/CLAUDE.md: nota sobre /full-git-push y dataforge/<name>+master.
- .claude/rules/apps_tbd.md: tronco unico master + init.defaultBranch.
- cpp/functions/core/app_menubar.md: notas del submenu Settings con About.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:41:55 +02:00
egutierrez 9aaf336c1a feat(commands,bash): estandarizar todos los apps y analyses como dataforge/<name>
- /full-git-push y /full-git-pull descubren apps/analyses sin .git y los
  inicializan/clonan automaticamente contra dataforge/<basename>.
- ensure_repo_synced.sh: localizar gitea_create_repo.sh / gitea_push_directory.sh
  via FN_REGISTRY_INFRA_DIR o FN_REGISTRY_ROOT (mas robusto al sourcing
  desde directorios arbitrarios y desde zsh).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:18:20 +02:00
egutierrez 424e913566 feat(cpp,bash): app_about + Settings submenu, ensure_repo_synced pipeline
cpp/core: nuevo modulo app_about — ventana About con project/version/desc,
componible via about_window_set_info() en el init de la app y rendererizada
automaticamente por fn::run_app al final de cada frame.

app_menubar: el item top-level "Settings..." pasa a ser un BeginMenu
"Settings" con dos subitems: "Settings..." (existente) y "About..." (nuevo).

bash/infra: nueva pipeline ensure_repo_synced que compone gitea_create_repo
y gitea_push_directory para garantizar repo Gitea existente + sync de un
directorio local en una sola llamada idempotente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:05:31 +02:00
egutierrez dc5e3266ed feat(commands): /full-git-push y /full-git-pull
Sincronizan el repo principal y todos los sub-repos git anidados (apps
externalizadas, projects con repo propio) y luego ejecutan fn sync para
sincronizar metadata no regenerable contra registry_api.

Credenciales para fn sync vienen de pass (registry/{api-token,
basicauth-user,basicauth-pass}).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:42:04 +02:00
egutierrez b149323045 feat(infra): set_exe_icon — embed icono .ico en .exe Windows post-build
Implementacion Go pura sin dependencias externas (sin rcedit, wine, ni rsrc).
Parsea ICONDIR + ICONDIRENTRY del .ico, construye un IMAGE_RESOURCE_DIRECTORY
tree con RT_ICON + RT_GROUP_ICON, y appendea una nueva seccion .rsrc al PE.
Soporta PE32 y PE32+. No soporta exe que ya tienen recursos (retorna error).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:41:56 +02:00
egutierrez 35aca86541 fix(infra): build tag !windows en process_kill/spawn/wait
Estas funciones usan syscall.Kill, Setpgid y ProcessKill (no disponibles
en Windows). Sin el build tag, el paquete functions/infra no cross-compila
para Windows desde apps que solo usan otras funciones del paquete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:41:49 +02:00
egutierrez fa1050841d docs(issues): añadir 0037-0040 — extraccion de entidades y relaciones
- 0037: IoC regex extractor (IP, email, dominio, hash, wallet, CVE, MAC)
- 0038: GLiNER entity extractor (zero-shot NER multilingue)
- 0039: GLiREL relation extractor (zero-shot triplets)
- 0040: pipeline hibrido extraccion grafos (regex + GLiNER + GLiREL + LLM fallback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:41:44 +02:00
egutierrez b36f6fe281 chore(python): añadir google-cloud-bigquery-datatransfer y google-cloud-storage
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:41:37 +02:00
egutierrez bd95dc5f99 docs(issues): marcar 0028, 0029, 0031, 0032, 0034 completados
Wave 2 de parallel-fix-issues integrada a master:
- 0028: ImPlot3D vendored como submodule + surface_plot_3d real + scatter_3d
- 0029: mesh_viewer + mesh_obj_load + mesh_gpu + orbit_camera
- 0031: tween_curves + bezier_editor + timeline (animation core)
- 0032: sql_workbench (text_editor SQL + table_view + sqlite3)
- 0034: scientific viz (treemap squarified, sankey BFS, chord, contour
  marching squares, voronoi raster)

primitives_gallery linux + windows cross-compile OK. fn index registra los
nuevos primitivos. exe Windows actualizado en Desktop/apps/primitives_gallery/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:59:54 +02:00
egutierrez dda03b14cc merge: issue/0034 — scientific viz (treemap, sankey, chord, contour, voronoi)
# Conflicts:
#	cpp/apps/primitives_gallery/CMakeLists.txt
#	cpp/apps/primitives_gallery/demos.h
#	cpp/apps/primitives_gallery/main.cpp
2026-04-25 21:55:49 +02:00
egutierrez 2f6dea6ebd merge: issue/0032 — sql_workbench
# Conflicts:
#	cpp/apps/primitives_gallery/CMakeLists.txt
#	cpp/apps/primitives_gallery/demos.h
#	cpp/apps/primitives_gallery/main.cpp
2026-04-25 21:55:17 +02:00
egutierrez 25936dff79 merge: issue/0031 — animation curves (timeline + bezier_editor + tween_curves)
# Conflicts:
#	cpp/apps/primitives_gallery/CMakeLists.txt
2026-04-25 21:54:48 +02:00
egutierrez 55939cb60b merge: issue/0029 — mesh_viewer + obj loader + orbit_camera
# Conflicts:
#	cpp/apps/primitives_gallery/demos.h
#	cpp/apps/primitives_gallery/main.cpp
2026-04-25 21:54:27 +02:00
egutierrez 0331293be3 merge: issue/0028 — ImPlot3D + surface_plot_3d + scatter_3d 2026-04-25 21:53:57 +02:00
egutierrez ca7ec360aa chore(issues): close 0034 — C++ scientific viz 2026-04-25 21:53:05 +02:00
egutierrez fdf7c82bd7 feat(primitives_gallery): demos para los 5 charts cientificos (issue 0034)
Anade 5 entradas a la gallery (treemap, sankey, chord, contour, voronoi)
con datos sinteticos coherentes:
- treemap: 6 items 'gastos por categoria'
- sankey: 8 nodos clientes -> productos -> categorias (DAG)
- chord: matriz 6x6 simetrica de flujos entre paises
- contour: mezcla de 2 gaussianas 32x32 + 5 niveles
- voronoi: 30 seeds aleatorias + colores aleatorios

Wire-up additions only (demos.h, main.cpp k_demos[], CMakeLists.txt).
2026-04-25 21:53:01 +02:00
egutierrez d0bce11a40 feat(viz): voronoi diagram via raster brute-force (MVP)
Para cada tile 4x4 px del rect de render: encontrar seed mas cercano
(distancia Euclidea) y rellenar con su color. Suficiente para N<=200
seeds en region <=600x400.

voronoi_layout deja polygon vacio en MVP — solo rellena seed/color.
Para extraer poligonos analiticos seria necesario half-plane
intersections (Fortune) — diferido a otro issue.
2026-04-25 21:52:52 +02:00
egutierrez cda557286e feat(viz): contour plot via marching squares — layout puro + render
contour_compute implementa marching squares clasico (16 casos, casos
ambiguos 5 y 10 partidos en 2 segmentos). Para cada level devuelve un
ContourLine{pts, level} con segmentos en coords [0..nx-1]x[0..ny-1].

Verificado con gaussiana 32x32 + 4 niveles: todos los endpoints aparecen
>=2 veces (curvas cerradas, ningun endpoint huerfano).
2026-04-25 21:52:48 +02:00
egutierrez 75d4334e8c feat(viz): chord diagram — arcos circulares + cuerdas bezier
Para una matriz NxN: cada nodo ocupa un arco proporcional a sum(row).
Las cuerdas matrix[i,j] son bandas bezier cubico hacia el centro
conectando los arcos de i y j.

Limitacion: las cuerdas se dibujan con AddConvexPolyFilled aunque la
forma no sea estrictamente convexa — visualmente queda razonable.
2026-04-25 21:52:43 +02:00
egutierrez 636c7b8d05 feat(viz): sankey diagram — BFS topologico + bandas curvas (bezier cubico)
compute_levels asigna columnas via BFS, los nodos se apilan verticalmente
proporcional a max(in_total, out_total). Los links se renderizan como
bandas con bezier cubico, color del nodo origen + alpha bajo.

Asume DAG (sin ciclos). Si hay ciclos, los nodos del ciclo quedan en su
nivel parcial — no rompe pero puede solapar visualmente.
2026-04-25 21:52:37 +02:00
egutierrez 0487180ac2 feat(viz): treemap squarified (Bruls et al.) — layout puro + render DrawList
treemap_layout devuelve TreemapRect{min, max, item} con coords absolutas
dentro de la region. La suma de areas == area total (verificado via test
standalone, ratio=1.000000). El render usa AddRectFilled + AddText cuando
labels y valores caben dentro de la cell.

Limitaciones MVP: jerarquia plana (no recursivo), sin interaccion.
2026-04-25 21:52:33 +02:00
egutierrez d42a385eb8 chore(issues): cerrar 0029 — mesh_viewer + obj loader + orbit_camera 2026-04-25 21:51:32 +02:00
egutierrez c1a3d72e59 feat(primitives_gallery): demo de mesh_viewer (cubo procedural + .obj loader)
Genera cubo procedural in-line (mesh_obj_parse de string), permite
cargar .obj desde un text input absoluto. Botones: Reload cube,
Wireframe toggle, Load .obj. Status line con tris count y
instrucciones (drag to orbit, wheel to zoom).

issue 0029
2026-04-25 21:51:27 +02:00
egutierrez b557433388 feat(viz): mesh_viewer — componente 3D con FBO + Lambert headlight
Compila/cachea por id un programa GLSL (vertex+fragment) con
iluminacion Lambert (luz=camara), gestiona Framebuffer cacheado por
id, dibuja MeshGpu con orbit camera, muestra via ImGui::Image y
maneja drag (mouse) + wheel (zoom). Wireframe opcional via
glPolygonMode.

gl_loader: añade glUniformMatrix4fv (proc requerido en Windows para
subir las matrices view/proj del mesh_viewer).

issue 0029
2026-04-25 21:51:22 +02:00
egutierrez 4e19583a22 feat(core): orbit_camera — camara orbital pura (matrices view/proj + drag)
State minimal (azimuth, elevation, distance, fov, aspect, near/far).
orbit_camera_matrices: lookAt+perspective row-major float[16] (subir a
GL con transpose=GL_TRUE). orbit_camera_handle_drag: dx→azimuth,
dy→elevation (clamp ±π/2-eps), wheel→distance (clamp >0.1). Sin glm,
solo <cmath>.

issue 0029
2026-04-25 21:51:15 +02:00
egutierrez c5dc63699a feat(gfx): mesh_gpu — VAO/VBO/EBO upload para Mesh
mesh_gpu_upload sube positions+normals interleaved (stride 6 floats,
location 0=a_pos, location 1=a_normal) + EBO uint32. mesh_gpu_destroy
libera todo. GL_STATIC_DRAW (mesh inmutable post-upload).

issue 0029
2026-04-25 21:51:10 +02:00
egutierrez 10ac4c74db feat(gfx): mesh_obj_load — minimal Wavefront .obj parser
mesh_obj_parse (pure) + mesh_obj_load (impure file helper).
Soporta v / vn / f (tris y quads). Genera normales per-face si
faltan (flat shading). Quads se parten en 2 tris; n-gons (>4) se
descartan silenciosamente. Indices 1-based positivos y negativos.

issue 0029
2026-04-25 21:51:05 +02:00
egutierrez cf3936314b chore(issues): close 0031 — animation curves shipped
Three primitives delivered + gallery demos + builds cleanly:
- tween_curves_cpp_core (16 Penner curves, header-mostly)
- bezier_editor_cpp_core (visual cubic Bezier editor)
- timeline_cpp_core (DAW-style keyframe widget)
2026-04-25 21:50:50 +02:00
egutierrez 4ed27a58d1 feat(primitives_gallery): demos for tween_curves + bezier_editor + timeline
Adds three new demos to the Core section of primitives_gallery:

- demo_tween: dropdown of all 16 Ease modes + animated plot showing the
  curve and a moving marker that traverses t=0..1 in a loop.
- demo_bezier_editor: live editor with reset + ease-out / ease-in-out
  presets, current control points displayed, slider over t showing
  bezier_eval(curve, t).
- demo_timeline: 2 tracks (hue, amp) with mixed eases, live progress bars
  showing track_value_at(current_time) updating each frame.

Wires the three demos into k_demos[] in main.cpp and adds the new sources
(plus the three function .cpp files) to CMakeLists.txt.
2026-04-25 21:50:44 +02:00
egutierrez 448765fa15 feat(core): add timeline — DAW-style keyframe widget
Timeline widget with:
- Header: play/pause + reset + duration drag + loop checkbox
- Ruler: 0.5s ticks, scrub via click+drag
- Tracks: horizontal rows with diamond-shaped draggable keyframes
- Playhead: vertical primary_light line + ruler triangle marker

State and types:
- Keyframe { time, value, ease }
- Track { name, vector<Keyframe> }
- TimelineState { tracks, current_time, duration, playing, loop }

Pure functions:
- track_value_at(track, t): interp between keys, ease applied via the
  destination keyframe (Maya/AfterEffects convention)
- timeline_update(state, dt): advance current_time, wrap or saturate

Render with fn_tokens for visual coherence with the rest of the design
system. Keys are sorted by time on every changed frame to keep order
consistent during drag.
2026-04-25 21:50:35 +02:00
egutierrez a8e153b73f feat(core): add bezier_editor — visual cubic Bezier curve editor
ImGui canvas with 4 draggable control points (p0/p3 locked at (0,0)/(1,1)
by default for use as easing curves). Pure evaluation via De Casteljau
(bezier_point) plus sampling-based y(x) lookup (bezier_eval).

Render uses fn_tokens for visual coherence: bg, border, primary curve,
text_dim diagonal reference, text_muted handle tangents.

p1.x and p2.x clamped to [0,1] to keep the curve usable as easing; Y
values are free to allow deliberate overshoot.
2026-04-25 21:50:29 +02:00
egutierrez 384bfecf3a feat(core): add tween_curves — Penner easing functions
16 easing curves (linear, quad, cubic, expo, elastic, bounce in/out/inOut)
header-mostly so the compiler inlines on hot paths. Pure, no I/O, no state.

Includes:
- tween::apply(Ease, t) dispatcher for data-driven uses (timeline keyframe.ease,
  dropdown selection)
- tween::name(Ease) for UI labels
- tween::ease_count for iteration

Tested values documented in tween_curves.md.
2026-04-25 21:50:21 +02:00
egutierrez 4d53ee6380 feat(primitives_gallery): demo de sql_workbench contra registry.db readonly
Demo nuevo en demos_sql.cpp: abre registry.db en SQLITE_OPEN_READONLY
(resolviendo via FN_REGISTRY_ROOT o cwd ascendente), monta
fn::SqlWorkbenchState con readonly=true y query inicial sobre la tabla
functions. Wire-up: entry en k_demos[] tras process_runner; declaracion
en demos.h; sources sql_workbench.cpp + demos_sql.cpp + link
SQLite::SQLite3 en CMakeLists.txt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:49:09 +02:00
egutierrez a757de0f05 docs: cerrar issue 0028 2026-04-25 21:48:55 +02:00
egutierrez 0cf04c37df feat(primitives_gallery): demos para surface_plot_3d + scatter_3d
- demos_3d.cpp con dos demos:
  * demo_surface_plot_3d: malla 64x64 de A*sin(fx*x)*cos(fy*y) con
    sliders fx/fy/amp en tiempo real.
  * demo_scatter_3d: 3 clusters gaussianos (N=500) coloreados por
    cluster, semilla fija para reproducibilidad.
- demos.h: declara las dos demos en la seccion Viz.
- main.cpp: dos entradas nuevas en k_demos[] (Viz, tras heatmap /
  table_view).
- CMakeLists.txt: anade demos_3d.cpp + surface_plot_3d.cpp +
  scatter_3d.cpp al target.

Issue 0028.
2026-04-25 21:48:51 +02:00
egutierrez e24d16db59 feat(core): sql_workbench component (issue 0032)
ImGui SQL workbench: editor (text_editor + CodeLang::SQL), schema sidebar
desde sqlite_master, tabla resultado (table_view), historial. Backend
sql_workbench_run_query separado de UI; readonly opt-in (rechaza non-SELECT/
PRAGMA/EXPLAIN/WITH); cap de filas (truncated_at, default 10000); last_ms
medido con steady_clock.

API: sql_workbench(id, sqlite3*, SqlWorkbenchState&, size). DB caller-owned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:48:50 +02:00
egutierrez cebf87cb4e feat(viz): surface_plot_3d real (ImPlot3D) + scatter_3d nuevo
surface_plot_3d (v2.0.0): quita el STUB. API basada en
SurfacePlot3DConfig (z[nx*ny] row-major + ranges X/Y) que delega en
ImPlot3D::PlotSurface. Las coordenadas X/Y por vertice se generan
internamente desde [x_min, x_max] x [y_min, y_max].

scatter_3d (v1.0.0): nuevo primitivo. Scatter 3D con tamano y color
opcionales por punto via ImPlot3DSpec::MarkerSizes / MarkerFillColors.
Util para PCA / clustering / nubes de puntos sinteticas.

Ambos namespace fn::, kind component, purity pure. Orbit / zoom / pan
los aporta ImPlot3D nativo.

Issue 0028.
2026-04-25 21:48:43 +02:00
egutierrez b3e55f7abe feat(framework): inicializar contexto ImPlot3D en app_base
ImPlot3D::CreateContext() / DestroyContext() acoplado al ciclo de
vida de ImPlot. Cualquier app que use fn::run_app obtiene el contexto
3D listo sin codigo extra.

Issue 0028.
2026-04-25 21:48:34 +02:00
egutierrez 4bf6f37e97 chore(vendor): vendorear ImPlot3D v0.4 como submodulo + target CMake
- submodule cpp/vendor/implot3d -> github.com/brenocq/implot3d, pinned
  commit 41ae3e447c0de20ecab95d38a4b4dc0835a3efc2 (v0.4).
- target CMake `implot3d` parejo a `implot` (3 sources: implot3d.cpp,
  implot3d_items.cpp, implot3d_meshes.cpp).
- fn_framework linkea implot3d para que cualquier app pueda usar las
  funciones viz/*_3d sin configurar nada extra.
- VENDORING.md externo (no tocamos el README upstream).

Issue 0028.
2026-04-25 21:48:29 +02:00
egutierrez 6d7b802199 feat(primitives_gallery): demos individuales para wave 1 + 5 primitivos viz/core
Cobertura del catalogo visual:
- text_editor (Wave 1, 0025): demo solo del editor con dropdown GLSL/SQL/Cpp/Generic.
- file_watcher (Wave 1, 0025): demo solo del watcher con boton touch + log.
- gl_texture_load (Wave 1, 0026): ya tenia demo (Gfx).
- process_runner: tarea simulada en background con spinner.
- candlestick: 30 dias OHLC sintetico.
- gauge: 3 indicadores con sliders (CPU/MEM/GPU).
- heatmap: gaussiana 12x12.
- table_view: 6 funciones del registry como muestra.

El demo combinado anterior (text_editor + watcher) se separa en dos para
que cada entry del sidebar exhiba un solo primitivo. Mas claro y mas
indexable.

Total entries en gallery: 26 (antes 23).
2026-04-25 21:40:44 +02:00
egutierrez c43f47a6b7 docs(rules): TBD obligatorio en apps generadas con fn (registry exento)
Aclara la politica:
- Registry (functions/, types/, .claude/, docs/, dev/issues/): push directo
  a master. Cambios atomicos por naturaleza, no hay deployment.
- Apps generadas (apps/, projects/*/apps/, services con tag service): TBD
  obligatorio — issue/<NNNN>-<slug> o quick/<slug>, merge --no-ff a master.

Tabla en apps_tbd.md de cuando aplica TBD por tipo de cambio. Indice de
reglas actualizado a 17 entradas.
2026-04-25 21:40:33 +02:00
egutierrez ab27e4e5fa build(cpp): primitives_gallery activa por defecto
Tras commitear los deps (button, toolbar, modal_dialog, app_settings, etc.)
todos los sources del gallery estan tracked. Quita el gate FN_BUILD_GALLERY
y deja el subdirectorio como add_subdirectory incondicional (igual que
chart_demo y shaders_lab).

Build verificado: cmake --build cpp/build construye chart_demo, shaders_lab,
text_editor_smoke, primitives_gallery y registry_dashboard.
2026-04-25 21:28:24 +02:00
egutierrez 2d35f5e1fb chore: añadir slash command /documentar y entrada de diario 2026-04-25 2026-04-25 21:26:12 +02:00
egutierrez 0db9242692 docs(cpp): añadir DESIGN_SYSTEM.md
Documenta el sistema de design tokens del registry C++:
- Identidad visual unica (Mantine v9 dark + indigo) compartida entre apps
- Como se aplican los tokens via fn::run_app
- Convenciones de uso (cuando usar tokens.colors.surface vs accent, etc.)

Es la fuente de verdad cuando se crean nuevas primitivas o apps fn_ui.
2026-04-25 21:26:09 +02:00
egutierrez a9045d45a0 feat(cpp/gfx): code_to_generator + shaderlab_db
Dos primitivas del pipeline de shaders_lab que ya estaban en uso pero sin
indexar al registry:

- code_to_generator_cpp_gfx (function, pure)
  Traduce un fragment shader GLSL escrito a mano (modo Code, con void main()
  + fragColor = ...) en un body de DAG Gen + DagControl[]. Cada uniform
  anotado se convierte en un control; el body usa el parametro uv y reemplaza
  fragColor= por return. Empaqueta uniforms en vec4 (4 x n_uniforms).

- shaderlab_db_cpp_gfx (function, impure)
  CRUD persistente para generators custom de shaders_lab via sqlite3.
  Guarda el GLSL original, el body traducido para el DAG, los DagControl y
  los param_defaults en una BD local (shaders_lab.db). Soporta open(:memory:)
  para tests.

Ambas se indexan ahora en registry.db y son reusables fuera de shaders_lab
si en el futuro hay otra app que componga DAGs de shaders.
2026-04-25 21:26:03 +02:00
egutierrez 8d28faf3e8 feat(cpp/core): app framework UI (settings, menubar, layouts)
5 primitivas que componen el chrome de una app fn_ui completa:

- app_settings_cpp_core         (function, impure)  ventana flotante de
  settings globales (Display + Typography + secciones extra registrables) con
  persistencia automatica en app_settings.ini junto al ejecutable.
- app_menubar_cpp_core          (component, pure)   MainMenuBar unificada
  con menu View (toggles de paneles) y Layouts. Punto de entrada de la
  menubar de cualquier app fn_ui.
- layouts_menu_cpp_core         (component, pure)   menu para guardar/aplicar/
  borrar/reset layouts de ImGui via callbacks (no I/O propio).
- panel_menu_cpp_core           (component, pure)   menu checkable para
  abrir/cerrar paneles, composable dentro de la MainMenuBar.
- layout_storage_sqlite_cpp_core (function, impure) primitivas CRUD de bajo
  nivel para persistir blobs INI de ImGui en sqlite (ui_layouts table).

Permiten que apps como shaders_lab y registry_dashboard ofrezcan: panel
toggles, layouts persistentes en BD, ventana de settings con preview en
vivo. Todas usan tokens_cpp_core para el styling.
2026-04-25 21:25:51 +02:00
egutierrez dff0d735c1 feat(cpp/core): primitivas UI estilo Mantine
Anade 9 primitivas reutilizables al registry C++ que replican el comportamiento
de los componentes correspondientes de @fn_library / Mantine v9, todas
estilizadas con tokens_cpp_core (colores Mantine dark + indigo):

- button_cpp_core         (component, pure)  variantes primary/secondary/subtle/danger + sm/md/lg
- icon_button_cpp_core    (component, pure)  cuadrado 28x28 con glyph centrado + tooltip
- toolbar_cpp_core        (component, pure)  grupo horizontal de acciones con separadores
- modal_dialog_cpp_core   (component, pure)  popup modal centrada + close con Escape
- text_input_cpp_core     (component, impure) InputText con label muted + placeholder
- select_cpp_core         (component, impure) dropdown con label + opcion '(none)' opcional
- toast_cpp_core          (component, impure) notificaciones efimeras + inbox con badge
- tree_view_cpp_core      (component, impure) jerarquia low-level con tree_node_clicked helper
- process_runner_cpp_core (component, impure) tarea en std::thread + spinner inline

Cada primitiva tiene su .md con frontmatter completo (params/output) y se
indexa via fn index. Son la base del primitives_gallery y de cualquier
app fn_ui futura.
2026-04-25 21:25:39 +02:00
egutierrez 79591daef2 feat(cpp/core): icon_font + icons_tabler
icon_font_cpp_core (impure): carga Karla-Regular como texto vectorial y
mergea Tabler Icons al mismo tamano en el atlas de ImGui. Tras el call,
los TI_* renderizan inline con el texto.

icons_tabler.h: header con macros TI_<NAME> que apuntan a los codepoints
del set Tabler (UTF-8 escapado). Generado a partir del CSS del vendor con
cpp/vendor/tabler-icons/gen_header.py — re-ejecutable si se actualiza el
set sin tocar a mano los ~5500 codepoints.

Justifica la regla cpp_icons.md: todas las apps C++ usan TI_* en lugar de
emoji o hex inline.
2026-04-25 21:25:25 +02:00
egutierrez 6e57818f14 chore(cpp/vendor): vendor Tabler Icons v3.41.1 (TTF + generador)
Vendorea el set de iconos Tabler (~5500 iconos, MIT) para usar en apps
ImGui via icon_font_cpp_core. Incluye:

- tabler-icons.ttf       — atlas TTF para mergear en ImGui
- tabler-icons.css       — referencia de codepoints (no se compila)
- gen_header.py          — script que regenera cpp/functions/core/icons_tabler.h
                           con macros TI_<NAME> a partir del CSS
- LICENSE / README.md    — atribucion MIT

Versionar el TTF (~700 KB) evita depender de descargas en build. Las apps
usan TI_* en strings y el atlas se carga con icon_font_cpp_core::load().
2026-04-25 21:25:17 +02:00
egutierrez 1ce5a97a50 chore: gitignore .claude/scheduled_tasks.lock 2026-04-25 21:25:09 +02:00
egutierrez 53402d84d5 docs(issues): marcar 0025 y 0026 como completados + WIP master
Wave 1 de parallel-fix-issues integrada a master:
- 0025: text_editor_cpp_core + file_watcher_cpp_core
- 0026: gl_texture_load_cpp_gfx (vendor: stb_image v2.30)

Ademas se commitea WIP previo de master que estaba sin commitear (cambios
en shaders_lab, dag_*, framework, tokens, kpi_card, gl_loader.md, etc.)
para dejar HEAD buildable.

Notas:
- Algunos deps del gallery (button.cpp, toolbar.cpp, modal_dialog.cpp...)
  siguen UNTRACKED — gating con FN_BUILD_GALLERY=ON (default OFF) para
  que master build (sin flag) no los necesite.
- Build OK con y sin flag. fn index registra 904 functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:14:15 +02:00
egutierrez 37e8139c5b feat(primitives_gallery): wire gl_texture_load demo
Tras integrar 0025 (que trajo la app entera) y 0026 (que solo añadio
demos_gl_texture.cpp + assets/sample.png + cpp/functions/gfx/gl_texture_load.*),
falta wire-up de la entrada nueva en los archivos compartidos del gallery.

- demos.h: declarar gallery::demo_gl_texture()
- main.cpp: entrada en k_demos[] con id "gl_texture", category "Gfx"
- CMakeLists.txt: añadir demos_gl_texture.cpp, gl_texture_load.cpp,
  stb_image_impl.cpp, e include dir vendor/stb

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:05:45 +02:00
egutierrez 5e94dd5087 merge: issue/0026 — gl_texture_load (stb_image)
# Conflicts:
#	cpp/CMakeLists.txt
2026-04-25 21:05:11 +02:00
egutierrez b228fd2d51 merge: issue/0025 — text_editor + file_watcher 2026-04-25 21:04:48 +02:00
egutierrez 9454194de0 docs: cerrar issue 0025
Implementado text_editor_cpp_core (PIMPL sobre ImGuiColorTextEdit MIT) y
file_watcher_cpp_core (inotify Linux / ReadDirectoryChangesW Win) con demo
combinada en primitives_gallery + smoke test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:01:32 +02:00
egutierrez ed1ffd07b5 feat(primitives_gallery): wire text_editor + file_watcher demo
- demos_text_editor.cpp: split horizontal con editor GLSL precargado a la
  izquierda (boton Save to /tmp/fn_demo.glsl + dirty indicator) y panel de
  eventos a la derecha (path, active flag, lista scrollable, boton clear).
  Watcher activo sobre /tmp/fn_demo.glsl; reintenta el add() tras el primer
  Save si el archivo no existia al iniciar.
- demos.h: declaracion de gallery::demo_text_editor()
- main.cpp: entry "text_editor"/"text_editor + watcher" en categoria Core
- CMakeLists.txt: anade demos_text_editor.cpp + sources de text_editor,
  file_watcher y vendor TextEditor.cpp + include path de imgui_text_edit

Nota: la primitives_gallery NO se construye en este branch (sus deps —
button.cpp, toolbar.cpp, etc. — son untracked en master). El subdirectorio
se anade pero protegido por FN_BUILD_GALLERY=OFF para no romper builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:00:55 +02:00
egutierrez dc079a815b test(cpp): add text_editor_smoke build gate for issue 0025
App minima (no abre ventana ImGui) que crea/settea/lee text_editor y registra
un watch sobre /tmp/fn_smoke_test.txt para confirmar que TextEditor.cpp del
vendor + text_editor.cpp + file_watcher.cpp enlazan correctamente. Activada
por defecto si la carpeta existe (no requiere la primitives_gallery).

Tambien anade flag FN_BUILD_GALLERY (OFF default) para no romper el build
cuando la primitives_gallery no esta presente — sus deps (button.cpp,
toolbar.cpp...) son sources untracked en algunas branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:00:47 +02:00
egutierrez b43be6a308 feat(cpp/core): add file_watcher_cpp_core (inotify Linux / RDCW Win)
Watcher de archivos no bloqueante con backend nativo por plataforma:

- Linux: inotify_init1(IN_NONBLOCK | IN_CLOEXEC), inotify_add_watch con
  mascara MODIFY|CREATE|DELETE|CLOSE_WRITE|MOVED_*. Drain en cada poll().
- Windows: ReadDirectoryChangesW overlapped + GetOverlappedResult no
  bloqueante. Para vigilar un archivo, registra el directorio padre y filtra
  por nombre exacto en el poll().
- Otros: stub — poll() devuelve vacio y last_error() reporta no soportado.

API en namespace fn:: con tipos opacos (FileWatcher PIMPL). Errores via
last_error() en vez de excepciones. Documentadas las limitaciones (limite de
inotify watches en Linux, granularidad directorio-level en Windows, no
recursividad).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:00:38 +02:00
egutierrez 3ad63f79dc feat(cpp/core): add text_editor_cpp_core component (PIMPL)
Wrapper en namespace fn:: sobre ImGuiColorTextEdit. La API publica solo expone
TextEditorState como tipo opaco; el TextEditor del vendor vive dentro del .cpp.

Soporta CodeLang::{Generic, GLSL, SQL, Cpp} (highlighting via las
LanguageDefinition del vendor). text_editor_render() devuelve true en el frame
en que el contenido cambia; flag dirty manejado independientemente para casos
"editado pero aun no guardado".

text_editor_get_text() cachea el resultado en un std::string del state para
mantener el const char* valido entre llamadas (el GetText() del vendor devuelve
std::string por valor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:00:31 +02:00
egutierrez 7cccaec5dd feat(cpp/vendor): vendor ImGuiColorTextEdit (MIT) for text_editor primitive
Pinneado al commit 0a88824f del upstream. Patches locales aplicados a
TextEditor.cpp para compilar contra ImGui 1.91+ (GetKeyIndex eliminado,
PushAllowKeyboardFocus/PopAllowKeyboardFocus removidos). Documentado en
cpp/vendor/imgui_text_edit/README.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:00:22 +02:00
egutierrez a27884ed0a docs: cerrar issue 0026
gl_texture_load_cpp_gfx implementado, vendoreado stb_image v2.30,
gl_loader extendido con glActiveTexture y glGenerateMipmap. Demo
demos_gl_texture.cpp + asset PNG damero 256x256 listos para la
primitives_gallery cuando se integre en master.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:59:03 +02:00
egutierrez f33d55485e feat(primitives_gallery): demo de gl_texture_load + asset PNG
Añade demos_gl_texture.cpp (carga assets/sample.png con la nueva
funcion gl_texture_load_cpp_gfx, muestra w/h/channels y sliders de
tint RGB + zoom UV via ImGui::ImageWithBg) y un PNG damero 256x256
generado para la demo.

Tambien registra apps/primitives_gallery como subdirectorio en
cpp/CMakeLists.txt (la app vive como WIP en master; aqui solo se
añade el hook de build).

Para integrarse, demos.h, main.cpp y CMakeLists.txt de la gallery
deben anadir respectivamente la declaracion gallery::demo_gl_texture(),
la entrada {"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}
en k_demos[], y demos_gl_texture.cpp + cpp/functions/gfx/gl_texture_load.cpp +
cpp/vendor/stb/stb_image_impl.cpp + include de cpp/vendor/stb a sus sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:58:50 +02:00
egutierrez cb0591ff91 feat(gfx): añadir gl_texture_load_cpp_gfx
Funcion impura del registry que carga PNG/JPG/BMP/TGA/HDR a una textura
OpenGL lista para sampler2D. Composable con gl_loader, gl_shader,
shader_canvas. Genera mipmaps, soporta sRGB y HDR (RGBA16F).

API:
  GlTexture gl_texture_load(path, flip_y=true, srgb=false)
  GlTexture gl_texture_load_from_memory(data, size, ...)
  void      gl_texture_destroy(tex)
  const char* gl_texture_last_error()  // thread-local
  void      gl_texture_bind_uniform(prog, name, tex, unit)

Errores via thread_local string accesible por gl_texture_last_error().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:58:14 +02:00
egutierrez fcbda2af2e feat(gl_loader): añadir glActiveTexture y glGenerateMipmap
Necesarios para que gl_texture_load (cpp/functions/gfx/) funcione en
Windows tras wglGetProcAddress. En Linux son simbolos directos via
GL_GLEXT_PROTOTYPES, no afecta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:58:07 +02:00
egutierrez 8402834487 chore(vendor): añadir stb_image v2.30 a cpp/vendor/stb/
Header-only image loader (public domain). Pinneado al commit
f0569113c93ad095470c54bf34a17b36646bbbb5 de nothings/stb.

Layout:
- stb_image.h — header (no modificar)
- stb_image_impl.cpp — UNICO TU que define STB_IMAGE_IMPLEMENTATION
- README.md — origen + commit + uso

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:58:02 +02:00
egutierrez 461bb77298 docs(issues): añadir 0025-0036 — features C++ para registry y primitives_gallery
12 issues nuevos para implementacion paralela: text_editor, file_watcher,
gl_texture_load, gl_compute+pingpong+DAG Compute, ImPlot3D, mesh_viewer,
audio reactivo, animation curves, sql_workbench, http+ws inspector,
scientific viz (5 charts), map_tiles, image_canvas + webcam_texture.

Cada issue añade funciones al registry y un demo propio en
primitives_gallery/demos_<feature>.cpp para minimizar conflictos en paralelo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:48:18 +02:00
egutierrez ab3115ce99 feat(shaders_lab): per-node preview thumbnails + double-rclick delete node
- DagStep: preview_open flag (default false).
- dag_compile: emit `uniform int u_preview_target` and a series of
  early-return branches at the start of fragColor selection. -1 (default)
  falls through to the real Output-driven fragColor.
- dag_node_previews (new fn): per-node FBO keyed by editor_uid, lazy
  created. Renders each node with preview_open=true to its FBO by
  setting u_preview_target = step index. Texture exposed via
  dag_preview_texture(uid) for ImGui::Image.
- dag_node_editor: small toggle button "[+] preview"/"[-] preview" in
  each non-Output node; when open, ImGui::Image(96x64, V-flipped).
- dag_node_editor: double right-click on hovered node deletes it
  (Output is protected).
- main.cpp: dag_previews_render after Canvas DAG; dag_previews_destroy
  on shutdown.

Single GL program drives both the canvas and all thumbnails — moving
sliders never recompiles, only the topology change does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:06:50 +02:00
egutierrez cd445c5833 feat(shaders_lab): pins straddle node edges (half outside, half inside)
draw_pin_circle takes a PinSide and centers the circle exactly on the
left or right edge of the node. The reserved Dummy is half-width
(PIN_RADIUS instead of PIN_DIAMETER) so the inside layout stays
compact, and ed::PinRect is set to the full circle so the protruding
half is still grabbable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:59:01 +02:00
egutierrez 3aa1821346 feat(shaders_lab): pins on node edges, neutral color, right-click clears
- Pin radius bumped to 9px (18px diameter). Easier to grab.
- Single neutral pin/cable color across all kinds (data is uniformly
  vec4 — coloring by node kind was misleading).
- ed::StyleVar_NodePadding(0,8,0,8): zero left/right padding so the pin
  circles sit flush with the node's edges, separated from the title
  and controls by 8px gaps.
- Right-click on a pin clears all connections on that pin:
  - input pin → clear that single slot's source_id
  - output pin → clear every source_id across the pipeline pointing
    to this node (fan-out).
- Right-click on a link still deletes that single link (existing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:52:09 +02:00
egutierrez f88a54ff41 feat(shaders_lab): big colored pin circles, side layout, right-click delete
- Three-column node layout: input pins on the left edge, controls in
  the middle, output pin on the right edge.
- Pins rendered as 14px filled circles with darker outline. Color is
  the node's kind (gen=blue, op=violet, blend=amber, output=red).
- ed::PinPivotAlignment(0,0.5)/(1,0.5) so the cable starts/ends at
  the circle center.
- Empty columns (gen with no inputs, output with no output) get a
  pin-sized Dummy so column widths stay consistent.
- ed::Link now passes color (= source node kind) and 2.5px thickness.
- Right-click on a link deletes it immediately via
  ed::ShowLinkContextMenu (no popup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:46:17 +02:00
egutierrez fb9b69cd6a fix(shaders_lab): cycle check uses real reachability, not vector index
Previously the cycle validator rejected any link whose source had a
vector index >= target's, which silently killed legitimate connections
between nodes added in the wrong drop order.

Switch to a DFS over source_ids: an edge from->to creates a cycle iff
`from` already (transitively) depends on `to`. topo_sort runs after
each topology change so the vector ends up in a consistent order
regardless of how nodes were inserted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:37:50 +02:00
egutierrez 161496a305 fix(shaders_lab): Output connectable after drop + working color picker
Two bugs:

1. Dropped nodes were pushed to the end of the pipeline, but the Output
   node already sat there from startup. The cycle validator and the
   compiler only look for sources at indices strictly lower than the
   target, so new nodes were invisible to the Output. Fix: insert
   dropped nodes before the first Output; topo_sort also stable-moves
   Output nodes to the back.

2. ColorEdit3 with default flags rendered RGB text inputs alongside the
   swatch; clicking them dragged the node instead of opening the picker.
   Fix: NoInputs + NoLabel leaves only the swatch (a single item), and
   ed::Suspend/Resume wraps the call so the popup isn't clipped to the
   node or captured by the canvas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:37:56 +02:00
egutierrez dfbe26da01 fix(shaders_lab): drop zone no longer eats node/slider input + inline tests
The previous InvisibleButton captured mouse events, so you could drag
from the Functions palette into the canvas, but node dragging and
slider interaction inside the canvas stopped working.

Fix: watch the global drag-drop payload without an explicit target. When
the mouse releases LMB over the DAG window with a "DAG_NODE_TYPE"
payload active, queue an add at that canvas position. No button, no
capture.

Tests (compiled standalone with preprocessor defines):
- dag_compile: 6/6 asserts (empty, single gen, op chain, multi-source
  blend, Output-driven fragColor, unconnected-Output fallback).
- dag_catalog: 8/8 asserts (uniqueness, per-kind input invariants,
  exactly one Output, body_glsl present & returns, control param
  indices valid).
Build with:
  g++ -std=c++17 -Icpp/functions -DDAG_COMPILE_TEST cpp/functions/gfx/dag_compile.cpp cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_compile_test
  g++ -std=c++17 -Icpp/functions -DDAG_CATALOG_TEST cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_catalog_test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:24:39 +02:00
egutierrez 0be4b29a4b feat(shaders_lab): Output node + Functions palette with drag-drop
- DagKind::Output (new enum): terminal sink; compiler wires fragColor to its source_ids[0]
- dag_catalog: "output" node (1 input, red)
- dag_compile: skips Output in node_<i> emission; final fragColor resolves from Output's connection
- dag_node_editor: no more Add button; drops "DAG_NODE_TYPE" payloads at mouse canvas position; Output cannot be deleted; Output has no output pin
- dag_palette (new fn): Functions window with grouped, draggable node cards
- main.cpp: "Functions" window added; ensure_dag_default seeds plasma + connected Output
2026-04-24 22:16:47 +02:00
egutierrez e91e80bfcf fix(shaders_lab): node drag was overwritten by SetNodePosition each frame
Previously SetNodePosition was called after EndNode every frame, which
reset any drag the user had done. Now the initial position is set once
per editor_uid (tracked in a static unordered_set), and the editor owns
the position afterwards. GetNodePosition at end-of-frame keeps step
state in sync for future persistence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:04:07 +02:00
egutierrez 88fca7b128 feat(shaders_lab): visual node editor (imgui-node-editor) + multi-source
- cpp/vendor/imgui-node-editor: vendorized thedmd/imgui-node-editor v0.9.4
- cpp/functions/gfx/dag_node_editor: new visual pipeline editor replacing dag_panel
- DagStep: source_ids[4] + editor_pos + editor_uid (multi-input support)
- DagNodeDef: num_inputs explicit; circle reclassified as Op
- dag_compile: N inputs per node, topological ordering preserved
- main: use node editor; destroy on shutdown
- patch imgui_extra_math.inl: guard operator*(float, ImVec2) for imgui >= 18955

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:56:48 +02:00
egutierrez bf5011de93 feat(shaders_lab): two simultaneous canvases (Code + DAG)
Canvas Code and Canvas DAG are now independent windows, each with its
own ShaderCanvas, FBO and compiled program. Both render every frame in
parallel. No more focus-based recompile — each source compiles when its
own content changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:50:29 +02:00
egutierrez 1a6e3cbeaf feat(cpp/viz): static-plot primitive + tooltips + rotated labels + card compacta
Nuevo primitivo compartido:
- cpp/functions/viz/plot_static.h: header-only con flags ImPlotFlags /
  ImPlotAxisFlags agrupados (NoFrame|NoMenus|NoBoxSelect|NoMouseText +
  Lock|NoInitialFit|NoHighlight) para visualizacion estatica en
  dashboards. Lo usan todos los charts de viz/.

Charts refactorizados a v1.1 con parametro `height` explicito (rompe el
feedback loop con contenedores AutoResizeY que producia vibracion al
redimensionar) y ejes pineados con ImPlotCond_Always:
- bar_chart v1.2: tooltip al hover (label + valor) + auto-rotacion de
  labels a 45 cuando no caben horizontalmente (medidos con CalcTextSize
  vs ancho del plot). Los labels rotados se dibujan manualmente con
  ImDrawList::PrimQuadUV + ImFontBaked::FindGlyph (API ImGui 1.92+).
- pie_chart v1.1: tooltip por slice (detecta cual via atan2 desde centro
  en sentido CCW matematico, que es como ImPlot dibuja los slices desde
  angle0=90) con label + valor + porcentaje. Aspect 1:1 mantenido.
- line_plot, scatter_plot, histogram v1.1: ejes pineados con limites
  calculados de min/max + 5% headroom (histogram usa AutoFit por los
  bins dinamicos, con Lock para bloquear pan/zoom).

kpi_card v1.2: card mas compacta — altura 78px (antes 108), font scale
1.4x (antes 1.8x), padding sm (antes md). Apto para densidades altas
de KPIs en dashboards.

fullscreen_window v0.2: NoScrollbar|NoScrollWithMouse para eliminar el
scrollbar fugaz que aparecia cuando el contenido excedia por 1-2px la
ventana, reflow de ancho y vibracion visible al redimensionar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:31:00 +02:00
egutierrez c974eaa604 feat(shaders_lab): independent windows for Code, DAG, Controls, Canvas, GLSL
Remove Code/DAG toggle. Each panel lives in its own dockable window.
Active source (Code or DAG) is whichever window has focus; Canvas title
shows the current active source. Switching active source triggers an
immediate recompile so the canvas reflects the selected pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:27:50 +02:00
egutierrez 2bb49e31c0 feat(shaders_lab): drag-and-drop to reorder DAG steps
ImGui native BeginDragDropSource/Target over each CollapsingHeader.
Drag source adjusted if ahead of target to keep final position correct.
Move Up/Down buttons kept for keyboard/touchpad fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:21:58 +02:00
egutierrez 5d83c11169 feat(shaders_lab): DAG pipeline mode with node catalog
- cpp/functions/gfx/dag_types: DagStep, DagNodeDef, DagControl (header-only)
- cpp/functions/gfx/dag_catalog: 10 hardcoded nodes (4 gen, 3 op, 3 blend) ported from shader-dag-blends.jsx
- cpp/functions/gfx/dag_compile: pipeline → GLSL 330 core with fan-in via source_id
- cpp/functions/gfx/dag_uniforms: upload u_params[16] via glUniform4fv
- cpp/functions/gfx/dag_panel: ImGui pipeline editor (add/remove/reorder/controls)
- main.cpp: Code/DAG mode toggle, per-mode compile path and uniforms
- gl_loader: +glUniform4fv
- rebuild Windows .exe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:15:21 +02:00
egutierrez c7821c4c99 feat(shaders_lab): uniform annotations → auto-generated ImGui controls
- cpp/functions/gfx/uniform_parser: regex-based parser of @slider/@color/@toggle/@xy annotations (+ inline tests)
- cpp/functions/gfx/uniform_panel: ImGui widgets + value store + glUniform* apply
- shader_canvas: optional uniforms callback invoked per-frame
- gl_loader: +glUniform1i/3f/4f
- seed plasma: demo uniforms u_speed + u_color
- rebuild Windows .exe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:02:35 +02:00
egutierrez 8e11c5cfce feat(cpp/viz): kpi_card v1.1 + bar_chart v1.1 — contenedor y altura fijas
kpi_card:
- v1.1: envuelve el contenido en BeginChild con surface bg + border +
  radius::md + padding::md (tokens). Replica Mantine Paper withBorder
  radius="md" p="md" usado en @fn_library/kpi_card.tsx.
- Ancho adaptativo via GetContentRegionAvail — requiere contenedor que
  propague ancho constrained (ImGui::BeginTable). dashboard_grid / BeginGroup
  no funcionan porque no constrainen ancho y la card desborda la celda.
- Linea de trend SIEMPRE visible: delta, sparkline, o em dash (text_dim)
  como placeholder, para que un grid de KPIs quede alineado vertical.
- Colores del delta via tokens (success/error) en vez de hardcoded ImVec4.

bar_chart:
- v1.1: altura explicita como parametro (default 200px). Sin esto, ImPlot
  con ImVec2(-1, 0) entra en feedback loop cuando esta dentro de un
  dashboard_panel (BeginChild con AutoResizeY): plot pide espacio -> padre
  se redimensiona -> plot recalcula. Efecto visual: las barras se deslizan
  los primeros frames.
- Ejes blindados: Lock + NoInitialFit + Cond_Always ademas de los flags
  previos. Y max pre-calculado con 15% de headroom.
- Sin inputs (NoInputs|NoFrame|NoBoxSelect|NoMouseText): estos charts son
  de resumen, no de exploracion.

Actualizados los .md correspondientes con el contrato visual + requisitos
de contenedor, para que cualquier dashboard que componga estos primitivos
obtenga el mismo look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:59:51 +02:00
egutierrez 4a95407d0e feat(shaders_lab): add gl_loader + Windows cross-compile
- cpp/functions/gfx/gl_loader.{h,cpp,md}: mini loader para OpenGL 2.0+
  (Linux no-op via GL_GLEXT_PROTOTYPES, Windows wglGetProcAddress)
- Portar gl_shader/gl_framebuffer/fullscreen_quad/shader_canvas al loader
- CMakeLists: WIN32_EXECUTABLE para lanzar sin consola en Windows
- apps/shaders_lab/shaders_lab.exe: binario PE32+ precompilado

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:52:37 +02:00
egutierrez 042bb43b37 feat(shaders_lab): scaffold C++ app with GLSL live-reload canvas
- cpp/functions/gfx: gl_shader, gl_framebuffer, fullscreen_quad, shader_canvas
- cpp/apps/shaders_lab: main + 3 seed shaders (plasma, circle, checker)
- ImGui docking layout: Code | Canvas | Controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 20:33:36 +02:00
egutierrez 5546ce6453 chore: untrack sqlite_api + registry.db, expand fn_monitoring docs
- sqlite_api se extrae a su propio repo Gitea (dataforge/sqlite_api),
  siguiendo la convencion de apps/*/ (cada app = su repo).
- registry.db ya estaba en .gitignore (regenerable con fn index +
  fn sync), pero seguia tracked por historia. Destracked.
- project.md de fn_monitoring ampliado con operacion completa:
  arranque del service (dev / start.sh / systemd user), flujo de
  datos dashboard, troubleshooting, como extender.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:22:33 +02:00
egutierrez fda89ca3ba feat(cpp/core): design tokens + primitivos UI para dashboards ImGui
Trasladar principios del DESIGN_SYSTEM.md de @fn_library (Mantine/React)
al mundo C++/ImGui sin añadir deps externas:

  cpp/functions/core/
    tokens       — colors/spacing/radius/font_size como constexpr +
                   apply_dark_theme() al ImGuiStyle global. Dark + indigo
                   primary (Mantine-inspired).
    badge        — etiqueta inline 6 variantes (Default/Success/Warning/
                   Error/Info/Outline). <Badge> de @fn_library en C++.
    empty_state  — placeholder centrado para tablas/listas vacías.
    page_header  — header con title + subtitle + separator + hueco
                   para acciones (patrón begin/end).

Scope limitado (KISS) a fases 1-2 del plan: tokens + 3 primitivos.
No se duplica dashboard_panel con un "card" — el existente ya cumple
el rol. Fases 3-5 (charts ImPlot line/area, app_shell con navbar,
toast/alert) quedan fuera hasta que el dashboard crezca en alcance.

Resultado:
- 869 funciones (+4) en registry.db.
- Dashboard con header homogéneo y empty states en todas las tablas.
- Sin hardcode de ImVec4 disperso en views.cpp.

Diary + CHANGELOG actualizados.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:52:09 +02:00
egutierrez a5a428b231 docs: registrar fix de subsystem:windows en diario y CHANGELOG
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:36:19 +02:00
egutierrez 7597549fcf docs: añadir ADR, diario, CHANGELOG y comando /entrada_diario
Infraestructura de documentación operativa y de decisiones:

- docs/adr/ — Architecture Decision Records. Incluye plantilla y
  ADR 0001 documentando el experimento y retirada de GitButler.
- docs/diary/ — diario de avances con un archivo por día.
  Primera entrada 2026-04-24.md retrocubriendo esta sesión
  (conectar aurgi-pc, dashboard fn_monitoring, funciones systemd
  locales, ADR GitButler, regla KISS).
- CHANGELOG.md — formato Keep a Changelog para cambios cara a
  usuario/agentes. Sección 2026-04-24 con Added/Changed/Fixed/Removed.
- .claude/commands/entrada_diario.md — slash command para añadir
  entradas al diario con formato consistente.

Separación:
  diary   = contexto operativo diario
  CHANGELOG = qué cambió en el código
  ADR     = por qué se decidió algo
  rules   = reglas operativas del agente

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:27:38 +02:00
egutierrez a92213853d docs(rules): añadir regla KISS para proyectos y apps
Formaliza la filosofía de mantener projects/ y apps/ simples:
- preferir herramientas del sistema o del registry antes que paquetes
  externos,
- cuestionar cada nueva dependencia (coste/beneficio, riesgo upstream),
- evitar abstracciones especulativas,
- ser consciente del flujo de trabajo actual.

Incluye el aprendizaje del experimento con GitButler (retirado en commit
correspondiente de repo_Claude) como caso concreto de una herramienta
externa que añadió complejidad sin beneficio en nuestro contexto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:13:29 +02:00
egutierrez c7ae46f86c feat(bash/infra): servicios systemd locales — 6 funciones atómicas + 1 pipeline
Nuevas primitivas para gestionar servicios systemd del sistema desde
el registry (antes solo había versiones remotas via SSH para deploy VPS):

  bash/functions/infra/
    systemd_local_install_unit   — escribir unit en /etc/systemd/system + daemon-reload
    systemd_local_enable         — systemctl enable
    systemd_local_start          — systemctl start + MainPID
    systemd_local_restart        — systemctl restart + MainPID
    systemd_local_status         — ActiveState/SubState/pid/enabled + journal tail (no sudo)
    systemd_local_uninstall      — stop + disable + rm unit + daemon-reload (idempotente)

  bash/functions/pipelines/
    install_systemd_service      — pipeline que compone las anteriores; args
                                    --name --exec [--workdir --user --env KEY=VAL
                                    --after --restart --type]

Requisito: sudo sin password para systemctl + escritura en /etc/systemd/system/.
En WSL: systemd=true en /etc/wsl.conf.

Registrado sqlite_api como servicio del sistema con este pipeline, queda
vivo al arrancar WSL. Dashboard ya no necesita arrancar la API manualmente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:02:54 +02:00
egutierrez 350d3341f6 chore: gitignorar registry.db + registrar submódulo GLFW
- registry.db ahora gitignored y regenerable con 'fn index' +
  completable con 'fn sync'. Evita conflictos entre ramas / PCs.
- Re-registrar submódulo cpp/vendor/glfw en .gitmodules con path
  limpio (antes heredado con /home/lucas/...). Necesario para el
  cross-compile Windows del registry_dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:02:33 +02:00
egutierrez 89b61af84e el documento esta vacio, arregla el comando /meta_bigq para que no esté vacio 2026-04-23 13:28:33 +02:00
egutierrez 77eca4c52e docs: add sync_setup.md — procedure to link a PC to registry.organic-machine.com
Covers the client-side linking flow (not the server deploy, which
already lives in bash/functions/infra/setup_registry_api.md):

- Requisitos: server UP, fn binary, pass con 3 entradas, GPG desbloqueada.
- Paso 1: ~/.fn_pc (identidad).
- Paso 2: FN_REGISTRY_API + REGISTRY_API_TOKEN — 3 opciones (zshrc snippet
  desde pass, direnv por proyecto, a mano por sesión).
- Paso 3-4: verificar con fn sync status + primer fn sync.
- Troubleshooting table (401, 403, localhost:8420, GPG cache, etc).

docs/README.md enlaza al nuevo fichero. registry.db actualizada tras
primer sync contra el server desde esta sesión.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:41:48 +02:00
egutierrez 0a0fe8c997 feat(@fn_library): extract 2 components + improve 2 from Claude Design export
From: sources/frontend_designs/Ads Analytics Dashboard _standalone_.html

New components:
- funnel_chart_ts_ui — visualización de funnel de conversión con barras
  degradadas y tasa entre etapas como Badge semántico.
- heatmap_grid_ts_ui — matriz rows × cols con intensidad color-mix sobre
  el primary color. Genérica (day×hour, cohort, correlation...).

Improvements:
- alert_ts_ui v1.1.0 — añadidas variantes semánticas success, warning, info
  (antes: solo default y destructive).
- data_table_ts_ui v1.1.0 — prop opcional density: compact | cozy | roomy.
  No rompe API existente (default 'cozy' = comportamiento previo).

Barrel frontend/functions/ui/index.ts actualizado con los dos nuevos
exports y el type DataTableDensity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:20:37 +02:00
egutierrez 0883155432 feat(design-system): DESIGN_SYSTEM.md + prompts + extract-design command
- frontend/DESIGN_SYSTEM.md: contrato del @fn_library (regla suprema para
  Claude Design y agentes).
- frontend/design_prompts/: 11 plantillas de prompt (onboarding, dashboard,
  crud, detail, settings, auth, error, custom, handoff_integrate) +
  questionnaire numerado para arranque rapido.
- .claude/commands/extract-design.md: workflow de 10 pasos para extraer
  componentes nuevos y mejoras desde exports "standalone" de Claude Design
  al registry, sync al espejo fn-design-system y push a gitea+github.
- .claude/scripts/extract_design_bundle.py: decodificador del bundle
  (base64+gzip en manifest, nombra JSX por heuristica de header).
- .gitignore: ignorar subrepos/*/ (el mirror fn-design-system es repo
  propio con remotes dataforge/fn-design-system + gutierenmanuel/fn-design-system).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:46:00 +02:00
egutierrez 16140eda60 docs: actualizar README de issues — marcar 8 issues como completados (wave 1-3)
Issues integrados a master en esta serie:
- 0010 Auth System (JWT, passwords, OAuth2, RBAC, sessions)
- 0011 WebSocket & SSE Server
- 0014 File Upload & Storage (+ S3 stubs)
- 0016 Rate Limiting (token bucket)
- 0019 Structured Logging (slog-based)
- 0021 CRUD Generator generico
- 0022 Init Pipelines (scaffolding api/web/desktop/cli)
- 0024 Dashboard YAML split por tab

Total aprox: 54 funciones y 23 tipos nuevos en functions/infra/
+ 4 pipelines bash en bash/functions/pipelines/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:00:57 +02:00
egutierrez 86ba6064ed merge: issue/0022-init-pipelines — scaffolding pipelines bash (api, web, desktop, cli) 2026-04-18 18:00:10 +02:00
egutierrez 7cddf872e5 docs: cerrar issue 0022
Los 4 init pipelines (init_api_app, init_web_app, init_desktop_app,
init_cli_app) estan implementados, verificados end-to-end con `fn run`, e
indexados en registry.db como kind=pipeline + tag=launcher. Guia consolidada
en docs/init-pipelines.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:59:04 +02:00
egutierrez c8ebb5a9fd docs: guia consolidada docs/init-pipelines.md + regenerar registry.db
docs/init-pipelines.md: referencia rapida de los 4 init pipelines con tabla
resumen, arbol de decision, combinaciones comunes, FAQ y estructura
generada por tipo.

registry.db: re-indexado para registrar los 4 nuevos pipelines como
kind=pipeline, purity=impure, domain=pipelines, tag=launcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:58:53 +02:00
egutierrez d763d8144f feat: init_cli_app bash pipeline — scaffold Go CLI app con TUI opcional
Genera apps/{nombre}/ con main.go (subcommand routing via os.Args + switch),
cmd_version.go, cmd_status.go, Makefile (build/run/install/test/vet/clean),
.gitignore, go.mod y app.md. Sin cobra/urfave — consistente con el resto de
apps del registry.

Flag --with-tui anade model.go con un modelo Bubbletea fullscreen (lista
filtrable con bubbles/list, spinner con bubbles/spinner, dark theme con
lipgloss). main.go arranca la TUI con tea.NewProgram(m, WithAltScreen) si no
hay args; sino hace subcommand routing normal.

Verifica con go mod tidy + go vet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:56:55 +02:00
egutierrez 851a8dc690 feat: init_desktop_app bash pipeline — scaffold Wails desktop app
Genera apps/{nombre}/ con Wails v2 backend (main.go con embed frontend/dist,
app.go con struct App y bindings Greet/GetVersion) + frontend Vite+React+Mantine
con alias @fn_library.

Flag --with-db anade store.go con SQLite (schema items) y bindings CRUD
(ListItems, CreateItem); app.go se regenera con campo db.

wails.json con scripts pnpm, go.mod con replace a fn-registry, app.md con
framework wails+vite+react+mantine y dir_path correcto.

Verifica con go mod tidy al final. wails build requiere CLI instalado pero
el scaffold funciona sin el.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:54:55 +02:00
egutierrez dc0ebe8a49 feat: init_web_app bash pipeline — scaffold Go API + React frontend
Extiende init_api_app anadiendo la capa frontend: pnpm + vite + react +
@mantine/core. Genera frontend/ con vite.config.ts (proxy /api y /health al
backend + alias @fn_library a frontend/functions/ui), src/main.tsx con
MantineProvider, src/App.tsx con AppShell y src/pages/Home.tsx consumiendo
/api/v1/status.

Flags: --port N, --with-auth, --with-db (delegadas a init_api_app).

Docker compose para desarrollo. Verifica con pnpm install && pnpm build si
pnpm esta disponible (skippable con SKIP_PNPM_BUILD=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:52:34 +02:00
egutierrez da58501723 feat: init_api_app bash pipeline — scaffold Go HTTP API app
Genera apps/{nombre}/ con main.go (http_serve + router + middleware chain +
graceful shutdown), handlers.go (HTTPJSONResponse), config.go (env vars),
migrations/001_initial.sql, Makefile, .env.example, .gitignore, go.mod y
app.md con frontmatter correcto.

Flags opcionales:
  --port N      puerto default del server (default 8080)
  --with-auth   jwt_middleware + login/register + tabla users/sessions
  --with-db     store.go con helpers CRUD y setup SQLite
  --with-ops    stub para fn ops init

Compone 8+ funciones del registry (http_*, migration_up, password_*, jwt_*).
Verifica con go vet al final.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:50:35 +02:00
egutierrez 095f8dacb9 merge: issue/0010-auth-system — JWT, passwords, OAuth2, RBAC, sessions (13 fns, 6 tipos) 2026-04-18 17:45:30 +02:00
egutierrez 2a6e26b6ad docs: cerrar issue 0010 2026-04-18 17:44:41 +02:00
egutierrez af2366acb5 feat: rbac_check (pure), jwt_middleware, rbac_middleware
Fase 5 del issue 0010 — RBAC y middlewares de auth.
- rbac_check es pura: solo recorre la matriz roles/permisos
- jwt_middleware extrae token del header Authorization: Bearer, valida e
  inyecta claims en el context con una key privada (jwtCtxKey struct{})
- rbac_middleware requiere jwt_middleware antes; lee role de claims.Custom
- Helper JWTClaimsFromContext para acceder a las claims desde handlers
- 401 claro si RBAC se usa sin JWT antes (code: no_claims)
2026-04-18 17:44:04 +02:00
egutierrez 1e5dfa5193 feat: oauth2_auth_url (pure), oauth2_exchange, oauth2_refresh
Fase 4 del issue 0010 — cliente OAuth2 sin golang.org/x/oauth2.
- Oauth2AuthURL es pura: solo construye la URL con net/url
- Oauth2Exchange/Refresh hacen POST application/x-www-form-urlencoded
- ExpiresAt calculado como now + expires_in del proveedor
- Refresh conserva el token original si el proveedor no devuelve uno nuevo
- Tests con httptest.NewServer como mock del proveedor
2026-04-18 17:41:42 +02:00
egutierrez 9153a20384 feat: session_create, session_validate, session_cleanup
Fase 3 del issue 0010 — sesiones SQLite como alternativa a JWT.
- Tabla sessions creada con CREATE TABLE IF NOT EXISTS (autosetup)
- Tokens de 32 bytes aleatorios (crypto/rand) codificados en hex (256 bits)
- Indices en user_id y expires_at
- Prepared statements para evitar SQL injection
- SessionCleanup para eliminar expiradas periodicamente
2026-04-18 17:40:13 +02:00
egutierrez eff5771b03 feat: jwt_generate, jwt_validate, password_hash, password_verify
Fase 2 del issue 0010 — auth core:
- jwt_generate/validate: HS256 manual con crypto/hmac + crypto/sha256
- password_hash/verify: wrappers de golang.org/x/crypto/bcrypt (cost 12 default)
- JWT rechaza alg != HS256 para mitigar ataque 'alg=none'
- hmac.Equal para comparacion constant-time de firmas
2026-04-18 17:39:00 +02:00
egutierrez 1aab74467b feat: tipos auth (JWTClaims, Session, OAuthConfig, OAuthTokens, Permission, Role)
Fase 1 del issue 0010 — tipos base del sistema de auth en dominio infra.
Define las estructuras que usaran jwt_*, session_*, oauth2_* y rbac_*.

Añade dep golang.org/x/crypto/bcrypt para el hashing de passwords.
2026-04-18 17:37:19 +02:00
egutierrez a5d532c001 merge: issue/0011-websocket-sse — WebSocket + SSE (8 fns, 4 tipos)
# Conflicts:
#	registry.db
2026-04-18 17:33:32 +02:00
egutierrez d4fd36bb96 merge: issue/0014-file-upload — file upload/storage + S3 stubs (11 fns, 3 tipos)
# Conflicts:
#	registry.db
2026-04-18 17:33:28 +02:00
egutierrez 8175bf82c4 merge: issue/0021-crud-generator — CRUD generator genérico (9 fns, 4 tipos)
# Conflicts:
#	registry.db
2026-04-18 17:33:23 +02:00
egutierrez 0ac5a6c78b merge: issue/0019-structured-logging — slog-based structured logging (7 fns, 3 tipos)
# Conflicts:
#	registry.db
2026-04-18 17:33:18 +02:00
egutierrez be41c928af merge: issue/0016-rate-limiting — token bucket rate limiter (6 fns, 3 tipos) 2026-04-18 17:33:07 +02:00
egutierrez 5e283504b6 merge: issue/0024-dashboard-yaml-split-por-tab — cerrar issue (código en repo dataforge/auto_metabase) 2026-04-18 17:33:03 +02:00
egutierrez 17d0940e1d chore: gitignorear external/ y worktrees/
external/ contiene symlink a repo_Claude (skills).
worktrees/ es el directorio que usa parallel-fix-issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:32:54 +02:00
egutierrez 27ee150dcd docs: cerrar issue 0011 2026-04-18 17:30:28 +02:00
egutierrez 68f0e727cf feat: WebSocket upgrader, hub, send, broadcast, handler con tests (issue 0011 fase 3-4) 2026-04-18 17:29:37 +02:00
egutierrez 7b1a30890c feat: SSE handler, send y keepalive (issue 0011 fase 2) 2026-04-18 17:25:03 +02:00
egutierrez f4f68c3de5 docs: cerrar issue 0021 2026-04-18 17:18:11 +02:00
egutierrez db8188cad3 docs: cerrar issue 0014 2026-04-18 17:17:49 +02:00
egutierrez 6265133ac9 test(crud): cobertura completa de los handlers y generadores CRUD
Anade tests unitarios e integracion sobre SQLite in-memory:
- CRUDDefineResource: casos felices y rechazo de inputs invalidos
  (nombre vacio, tipos no soportados, nombres reservados, duplicados).
- CRUDGenerateTableSQL: columnas base, NOT NULL/UNIQUE/DEFAULT, deleted_at
  con soft_delete y verificacion de que el DDL es ejecutable en sqlite.
- Create + Get: creacion feliz, validaciones required/min_length/max_length/
  enum/min/max, 409 en UNIQUE, GET 200/404.
- List: paginacion, filtros, orden ascendente, campos desconocidos ignorados.
- Update: partial update, 404 y validacion de campos enviados.
- Delete: hard delete, soft delete, 404, ocultar soft-deleted en list.
- Integracion end-to-end con httptest.NewServer cubriendo CRUD completo y
  multiples recursos registrados en el mismo mux.
2026-04-18 17:17:42 +02:00
egutierrez 6185e8cbd0 feat: stubs s3_upload, s3_download, s3_presign_url (issue 0014 fase 4) 2026-04-18 17:17:19 +02:00
egutierrez 8c489f9a5f docs: cerrar issue 0019 structured logging
Funciones, tipos y tests implementados. Registry actualizado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:15:45 +02:00
egutierrez 55998e36ad feat: file_save_disk, file_delete, file_serve, upload_parse, upload_handler, thumbnail_generate (issue 0014 fase 3) 2026-04-18 17:15:39 +02:00
egutierrez 28599436e5 feat(crud): handlers HTTP y registro de rutas para recursos CRUD
Anade los 5 handlers CRUD genericos (list, get, create, update, delete) a
partir de un CRUDResource y *sql.DB, la factory crud_generate_handlers que
compone los 5 en un mapa, y crud_register_routes que registra todas las
rutas REST en un http.ServeMux con la sintaxis METHOD /path de Go 1.22+.

Caracteristicas:
- List con paginacion (page, per_page), orden (sort_by, sort_dir) y
  filtros exactos (filter_<campo>), validando nombres de columna contra
  la definicion del recurso para evitar SQL injection.
- Create valida required y validaciones (min/max, min_length/max_length,
  pattern, enum) antes de insertar; mapea UNIQUE violations a 409.
- Update hace partial update — solo los campos presentes en el JSON.
- Delete hace hard delete o soft delete segun CRUDResource.SoftDelete.
- UUIDs generados via github.com/google/uuid; timestamps en RFC3339Nano UTC.

Los handlers usan las funciones HTTP del registry (http_json_response,
http_error_response, http_parse_body) y se pueden componer con el mux
via http_router.
2026-04-18 17:15:33 +02:00
egutierrez 4c88adc183 feat(crud): tipos y generador de DDL para recursos CRUD
Anade los tipos CRUDResource, CRUDField, CRUDListParams y CRUDListResult
que modelan un recurso CRUD sobre SQLite, junto con dos funciones puras:
- crud_define_resource valida nombre, tabla y campos (tipos SQLite validos,
  nombres reservados, duplicados) antes de retornar el CRUDResource.
- crud_generate_table_sql genera el DDL CREATE TABLE IF NOT EXISTS con
  id TEXT PRIMARY KEY, timestamps estandar y, si aplica, deleted_at para
  soft delete.

Primera capa de 0021 — el resto (handlers + registro de rutas) se apoya
sobre estas estructuras.
2026-04-18 17:15:21 +02:00
egutierrez 893b98f5b9 docs: cerrar issue 0016 (rate limiting) 2026-04-18 17:14:50 +02:00
egutierrez 21cd03f154 test: cobertura de structured logging (logger_new, logger_with, log_*, logger_middleware)
- LoggerNew: formatos validos e invalidos, output nil, filtrado por nivel
- LoggerWith: anadir fields, no mutacion del base, apilamiento, nil-safe
- LogDebug/Info/Warn/Error: niveles correctos en JSON, campos variadicos, logger nil no panic
- LoggerMiddleware: method/path/status/duration_ms, default 200, preserva campos del logger

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:14:41 +02:00
egutierrez e030b105b1 feat: logger_middleware — middleware HTTP con logs estructurados (infra)
Middleware que envuelve cualquier http.Handler y emite un log info por
cada request con method, path, status y duration_ms. Hereda los campos
contextuales del Logger (app, version, request_id...) y se compone con
HTTPMiddlewareChain + HTTPCORSMiddleware.

Diferencia con http_logger_middleware: este escribe JSON estructurado via
slog en vez de texto plano a un io.Writer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:14:36 +02:00
egutierrez de297f1d41 feat: log_debug, log_info, log_warn, log_error (infra)
Funciones de nivel que delegan al *slog.Logger interno del Logger.
Todas son impuras y soportan logger nil sin panic (no-op).
Los fields se pasan como pares key-value variadicos estilo slog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:14:29 +02:00
egutierrez c3d9fbd8d3 feat(infra): rate limit middlewares HTTP por IP y por key + tests
Implementa fase 2 del issue 0016:
- rate_limit_middleware: limita por IP (X-Forwarded-For > X-Real-IP > RemoteAddr)
- rate_limiter_by_key: middleware configurable con keyFunc custom (API key, user ID...)
- Cuando se rechaza responde 429 con HTTPError JSON y headers Retry-After + X-RateLimit-*
- Tests con httptest.NewRecorder cubriendo: limite, burst, IPs independientes, XFF prioritario,
  recarga temporal, JSON body, headers IETF, GC stop idempotente, key vacia salta limit
2026-04-18 17:14:25 +02:00
egutierrez c5b4ffda65 feat: logger_new y logger_with sobre log/slog (infra)
- logger_new (impuro): construye *Logger con handler JSON/text segun formato
- logger_with (puro): clona el Logger anadiendo campos contextuales via slog.With

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:14:23 +02:00
egutierrez 1860cf2bc6 feat: tipos Logger, LogLevel y LogEntry para structured logging (infra)
Tipos base para las funciones de structured logging sobre log/slog:
- LogLevel: suma enum Debug/Info/Warn/Error
- Logger: wrapper producto con nivel, output, formato y fields contextuales
- LogEntry: modelo canonico JSON para tests y pipelines de logs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:14:19 +02:00
egutierrez 746d9dd4c9 feat: file_validate_type y file_unique_name puras (issue 0014 fase 2) 2026-04-18 17:12:05 +02:00
egutierrez 036c0a8d63 feat(infra): rate limiter token-bucket in-memory + tipos y core funcs
Implementa fase 1 del issue 0016:
- Tipos RateLimiter, RateLimitConfig y RateLimitResult en types/infra/
- rate_limiter_new: constructor con validacion rate/burst > 0
- rate_limiter_check: evalua token bucket por key, calcula Allowed/Remaining/ResetAt/RetryAfter
- rate_limit_headers (pure): construye headers IETF X-RateLimit-* y Retry-After
- rate_limiter_cleanup: goroutine GC de entries inactivas con stop idempotente

Sin dependencias externas (no Redis). sync.Mutex + map. Algoritmo token bucket
estandar con recarga lineal proporcional al tiempo transcurrido.
2026-04-18 17:11:22 +02:00
egutierrez f242441bb2 docs: cerrar issue 0024
El split de dashboards YAML por tab ya esta implementado y committeado en
apps/auto_metabase (repo dataforge/auto_metabase, commit 47b5f89):

- dashboard_split.py: helpers puros split_dashboard_payload() y merge_dashboard_parts()
- dashboard_split_test.py: 23 tests passing (round-trip, edge cases, real aurgi YAML)
- sync_pull.py: escribe directorio {slug}/_dashboard.yaml + tab_*.yaml
- sync_push.py: lee directorio o legacy monolitico, reconstruye payload unificado
- sync_validate.py: valida parent_slug, tab_ids y detecta archivos huerfanos
- payload.py: resolve dashboards/{slug}/_dashboard.yaml preferentemente
- app.md: documenta nuevo layout con seccion "Dashboard split por tab"
- Backward-compat con legacy dashboards/{slug}.yaml
- Aplicado a aurgi (dashboard id=734) con 9 tabs separados

Las apps viven en repos Gitea independientes (dataforge/auto_metabase), por
lo que los cambios de codigo no quedan reflejados en el historial de
fn_registry, solo el cierre del issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:10:36 +02:00
egutierrez 82b792415b feat: tipos UploadedFile, StorageConfig, S3Config en infra (issue 0014 fase 1) 2026-04-18 17:10:31 +02:00
egutierrez 2313f1aa55 feat: tipos WSHub, WSClient, WSMessage, SSEEvent (issue 0011 fase 1) 2026-04-18 17:10:28 +02:00
egutierrez 3bc2d2573d merge: quick/init-jupyter-project-support — init_jupyter_analysis v1.1.0 con soporte --project
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 15:40:13 +02:00
egutierrez 814262130d feat: init_jupyter_analysis v1.1.0 — soporte --project, --desc, --tags
Nueva funcion write_analysis_md_bash_infra genera analysis.md con frontmatter.
El pipeline ahora acepta --project para crear analisis directamente en
projects/{proyecto}/analysis/{nombre}/, valida que el proyecto exista,
genera analysis.md con dir_path correcto y ejecuta fn index al final.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 15:39:20 +02:00
egutierrez 69b626a426 merge: quick/temp-workspace — documentar workspace efimero temp/ 2026-04-16 23:42:06 +02:00
egutierrez c74d316f00 docs: añadir workspace efimero temp/
Documenta la carpeta temp/ como workspace desechable para pruebas rapidas
(APIs, scripts exploratorios, prototipos) y la añade a .gitignore.

Cambios:
- .claude/CLAUDE.md: incluye temp/ en el arbol de estructura del proyecto
  con la nota de que es efimero, gitignored y no indexado.
- .claude/rules/apps_vs_functions.md: nueva seccion "temp/ — workspace
  efimero" con las reglas de uso (no es codigo del registry, estructura
  libre, se extrae al registry si algo resulta util, se puede borrar).
- .gitignore: añade temp/ para que su contenido nunca se versione.

Impacto: los agentes y el humano tienen ahora un lugar claro donde
probar cosas sin contaminar el registry ni preocuparse por limpieza.
No toca codigo existente ni la estructura de apps/ o functions/.
2026-04-16 23:41:47 +02:00
egutierrez 046d21e721 feat: registry_api + fn sync — sincronización de registry.db entre PCs
Nuevo sistema para mantener datos no regenerables (proposals, apps, projects,
analysis, vaults, pc_locations) sincronizados entre múltiples máquinas via
una API HTTP central desplegada en organic-machine.com.

- Migración 011: tabla pc_locations (mapa de ubicaciones por PC)
- registry/models.go: struct PcLocation
- registry/store.go: CRUD PcLocation + helpers de sync
- cmd/fn/sync.go: subcomando fn sync (push+pull, status, locations)
- bash/functions/infra/setup_registry_api: pipeline de deploy Docker+Traefik
- CLAUDE.md: documentación de sync y pc_locations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 02:12:38 +02:00
egutierrez 1ecbf59d3b merge: quick/metabase-expansion-v2 — expansion Metabase: snippets, notifications, filters, export, ProseMirror 2026-04-14 19:03:56 +02:00
egutierrez 2ff1d0e847 chore: actualizar exports __init__.py y regenerar registry.db
Nuevos exports: snippets, notifications, dashboard_filters, export_card,
create_model, prosemirror_card_embed. registry.db regenerado con todas las
funciones nuevas indexadas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:03:27 +02:00
egutierrez cf70385bca feat(metabase): expansion cards y documents — export, model, ProseMirror validation, copy nativo
Cards: export_card (CSV/XLSX/JSON), create_model (type=model para fuentes MBQL).
Documents: prosemirror_card_embed helper (resizeNode envolviendo cardEmbed),
validacion automatica contra whitelist TipTap antes de enviar, copy_document
refactorizado al endpoint nativo POST /api/document/:id/copy.
Docs: dataset_query legacy vs MBQL5, template-tags, whitelist de nodos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:03:19 +02:00
egutierrez cb392a48ee feat(metabase): nuevos modulos — snippets, notifications, dashboard_filters
Tres modulos nuevos con funciones CRUD completas:
- snippets: list, get, create, update, archive (SQL reutilizable)
- notifications: list, create_card_alert, create_dashboard_subscription, update, delete
- dashboard_filters: add_dashboard_filter (parameter_mappings sobre cards existentes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:03:08 +02:00
egutierrez b0abc165f5 merge: quick/metabase-expansion-and-issues — expansion Metabase + issues tracker 2026-04-13 23:32:22 +02:00
egutierrez 9d9193a8cc docs(issues): nuevos issues de backlog y limpieza de tracker
- Añade issues nuevos al backlog: 0010 auth-system, 0011 websocket-sse, 0014 file-upload, 0016 rate-limiting, 0019 structured-logging, 0021 crud-generator, 0022 init-pipelines, 0024 dashboard-yaml-split-por-tab.
- Elimina 0008-sqlite-api-web.md (ya no aplica).
- Actualiza dev/issues/README.md con el estado del tracker.
2026-04-13 23:31:47 +02:00
egutierrez 4300f1242d feat(metabase): expansion de funciones Python — documents, collections, permissions, validation
Añade un conjunto amplio de funciones al paquete python/functions/metabase:
- Nuevos modulos: collections.py, documents.py, maintenance.py, permissions.py, validation.py (+ test).
- Ampliacion de cards.py, dashboards.py, client.py e __init__.py para exponer las nuevas operaciones.
- Funciones de documentos (create/get/update/delete/archive/copy/move + comentarios), grupos y memberships, permission/collection graphs, copy/move de cards y dashboards, validacion de MBQL/SQL y payloads, actualizacion segura de dashboards y fix_null_ratio.
- .md por funcion con frontmatter para que fn index los registre.
- Actualiza pyproject.toml y uv.lock con las dependencias resultantes.

Impacto: ampliamente mas cobertura de la API de Metabase desde el registry, reutilizable por apps y analisis. No toca Go ni frontend.
2026-04-13 23:31:42 +02:00
egutierrez e42c59de16 chore: untrack apps/auto_metabase (lives in its own repo dataforge/auto_metabase) 2026-04-13 14:28:26 +02:00
egutierrez 310b409ae0 feat(auto_metabase): push-all + describe/sql + auto-inject de dashcards
- push_all(): pushea todos los YAMLs de un proyecto (cards primero,
  dashboards despues), solo CREATE/UPDATE, resiliente a fallos por item
- explore.py: comandos describe (schema de DB) y sql (query ad-hoc con
  limite, cap 5MB, bloqueo de escrituras destructivas)
- payload.py: auto-inyecta id:-N, visualization_settings:{} y
  parameter_mappings:[] en dashcards nuevas para evitar 500 en push
- test_local: 11 cards + 3 dashboards sobre Sample Database de Metabase
- registry.db regenerado con auto_metabase_py_analytics indexada

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:14:05 +02:00
egutierrez 386a5471e0 docs: actualizar README de issues — marcar 8 issues de Wave 1 como completados
Wave 1 completada e integrada a master:
- 0009 HTTP Server Foundation
- 0012 Email & SMTP
- 0013 Background Job Queue
- 0015 Database Migrations
- 0017 Frontend Data Hooks
- 0018 Config & Env Management
- 0020 PDF Generation
- 0023 Testing Utilities

Pendientes (Wave 2 descartada, Wave 3 sin iniciar):
0010, 0011, 0014, 0016, 0019, 0021, 0022

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:26:05 +02:00
egutierrez 25d00e3518 fix: rename openTestDB to openMigrationTestDB to avoid redeclaration with job_queue_test.go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 02:07:27 +02:00
egutierrez 3304084121 merge: issue/0023-testing-utils — Go testing utilities
# Conflicts:
#	registry.db
2026-04-13 02:06:03 +02:00
egutierrez 0952fca70e merge: issue/0020-pdf-generation — PDF generation Python+Go
# Conflicts:
#	registry.db
2026-04-13 02:05:55 +02:00
egutierrez 32be754b60 merge: issue/0018-config-env — Config & env management
# Conflicts:
#	registry.db
2026-04-13 02:05:47 +02:00
egutierrez edfca3e73e merge: issue/0017-frontend-hooks — React HTTP hooks
# Conflicts:
#	registry.db
2026-04-13 02:05:39 +02:00
egutierrez 9da8a698b9 merge: issue/0015-db-migrations — SQL migration system
# Conflicts:
#	registry.db
2026-04-13 02:05:31 +02:00
egutierrez f433d0097b merge: issue/0013-background-jobs — SQLite job queue
# Conflicts:
#	registry.db
2026-04-13 02:05:20 +02:00
egutierrez 7404a9a977 merge: issue/0012-email-smtp — Email SMTP functions 2026-04-13 02:05:03 +02:00
egutierrez cb3ed99b9c merge: issue/0009-http-server — implementación paralela 2026-04-13 02:04:50 +02:00
egutierrez 39c85a5e9d docs: cerrar issue 0012 — email SMTP implementado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:04:11 +02:00
egutierrez 012b013372 docs: cerrar issue 0020 — PDF Generation implementado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:03:28 +02:00
egutierrez 86bd9f38a9 docs: cerrar issue 0015 — sistema de migraciones SQL implementado
6 funciones (migration_parse, migration_validate, migration_create, migration_up,
migration_down, migration_status) + 2 tipos (Migration, MigrationStatus) indexados
en registry.db. 26 tests, todos pasan. Build limpio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:03:19 +02:00
egutierrez 6488f70d13 chore: añadir fpdf2 y pypdf a python/pyproject.toml y uv.lock
Dependencias necesarias para pdf_create_py_infra, pdf_add_table_py_infra
y pdf_merge_py_infra. Instaladas previamente via uv add en el repo principal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:03:16 +02:00
egutierrez f4771485cf docs: cerrar issue 0017 — frontend hooks implementados 2026-04-13 02:03:15 +02:00
egutierrez be081c68f2 feat: funciones email SMTP en Python (infra)
smtp_send: conecta+envia+cierra en un paso via smtplib (TLS/STARTTLS/plain).
email_build_html: construye EmailMessagePy frozen dataclass con cuerpo HTML.
Solo stdlib Python: smtplib, email.mime. Tests con mock SMTP server threading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:03:12 +02:00
egutierrez c9a1c435fa feat: agregar hooks React HTTP genericos — issue 0017
4 tipos en frontend/types/core/: FetchState, MutationState, FormState, APIClientConfig.
9 funciones en frontend/functions/core/: api_client, http_cache, use_fetch,
use_mutation, use_infinite_scroll, use_form, use_debounced_search, use_sse, use_websocket.
Zero dependencias externas — solo React + fetch nativo. Cache in-memory con SWR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:03:10 +02:00
egutierrez 0819c35bbb feat: issue/0020 — generacion de PDFs en Python y Go
Añade 3 tipos Python (PDFDoc, PDFPage, PDFStyle) y 10 funciones Python
para construir PDFs con fpdf2 (builder fluent), fusionar PDFs con pypdf
y convertir HTML/Markdown a PDF via weasyprint (stub si no disponible).
Añade pdf_simple_report en Go como stub hasta que go-pdf/fpdf se integre.

- python/types/infra/: pdf_doc, pdf_page, pdf_style
- python/functions/infra/: pdf_create, pdf_add_page, pdf_add_text,
  pdf_add_table, pdf_add_image, pdf_add_header_footer, pdf_from_html,
  pdf_from_markdown, pdf_merge, pdf_save
- functions/infra/pdf_simple_report.go: stub Go con ReportSection/ReportTable
- 17 tests Python pasando (pytest)
- fpdf2 y pypdf añadidos via uv al venv Python

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:02:51 +02:00
egutierrez 203d3fd6b3 docs: cerrar issue 0018 — Config & Env Management implementado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:02:28 +02:00
egutierrez 4e3c9b5593 fix: corregir IDs de tipos en uses_types/returns a formato PascalCase
Los IDs de tipos Go usan PascalCase: Migration_go_infra, MigrationStatus_go_infra.
Actualizar los .md de todas las funciones migration para referenciar los IDs correctos.
Re-indexar: 681 funciones, 109 tipos, 0 errores de validacion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:02:27 +02:00
egutierrez c7998de8bb feat(infra): funciones Python config_from_env y dotenv_load con tests
- config_from_env_py_infra: dataclass + field metadata, tipos str/int/float/bool/list
- dotenv_load_py_infra: parser .env con semantica de no-sobreescritura
- 15 tests unitarios Python, todos PASS
- registry.db actualizado con fn index (685 funciones, 109 tipos)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:56 +02:00
egutierrez ebf246beb0 feat(infra): funciones impuras Go para carga de env/config (dotenv, env_require, config_from_env, config_from_file)
- dotenv_load: parser .env con no-sobreescritura y soporte de comillas
- env_require: os.Getenv con error descriptivo fail-fast
- env_require_all: verifica multiples vars y lista todas las faltantes
- config_from_env: reflection sobre struct tags env/default/required/secret, 5 tipos soportados
- config_from_file: JSON via stdlib, YAML stub con not-implemented
- 25 tests unitarios, todos PASS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:50 +02:00
egutierrez c3b007a4e7 feat(infra): tipos ConfigError y ConfigValidation + funciones puras Go (validate, merge, dump)
- ConfigError y ConfigValidation como tipos producto con sus .md en types/infra/
- config_validate: validacion con tags required/format/min/max/oneof via reflection
- config_merge: merge no-mutante de map[string]string con precedencia de override
- config_dump: serializacion de structs a map con mascara *** para campos secret
- 17 tests unitarios, todos PASS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:43 +02:00
egutierrez 35a49174ca feat: funciones impuras migration_create, migration_up, migration_down, migration_status
Fase 2 del issue 0015. MigrationCreate (crea archivo .sql template con version
auto-calculada), MigrationUp (aplica migraciones pendientes en transacciones
individuales), MigrationDown (revierte ultimas N via down_sql de _migrations),
MigrationGetStatus (cruza disco con BD, detecta orphaned). Tests de integracion:
ciclo completo create->up->status->down->status. 26 tests, todos pasan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:40 +02:00
egutierrez ec36278c7b feat: tipos Migration/MigrationStatus y funciones puras migration_parse + migration_validate
Fase 1 del issue 0015. Tipos Go en functions/infra/migration.go con metadata en
types/infra/. Funciones puras: MigrationParse (parsea filename NNN_name.sql +
bloques -- +up/-- +down) y MigrationValidate (verifica secuencia, huecos,
duplicados, bloques vacios). 16 tests, todos pasan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:34 +02:00
egutierrez 6019f2aafa test: tests Go para funciones email SMTP
Tests para builders puros (build_html, build_text, with_attachment),
template_render, smtp_connect y smtp_send con mock TCP server.
Todos los tests pasan: 18 casos cubriendo pureza, inmutabilidad y envio SMTP.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:18 +02:00
egutierrez 5d47bc3fe3 docs: cerrar issue 0023 — testing utilities implementadas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:16 +02:00
egutierrez ef3ae2aae9 feat: tipos y funciones email SMTP en Go (infra)
Tipos: EmailAttachment, EmailMessage, SMTPConfig.
Puras: email_build_html, email_build_text, email_with_attachment, email_template_render.
Impuras: smtp_connect (TLS/STARTTLS/plain), smtp_send (MIME multipart con adjuntos).
Solo stdlib: net/smtp, crypto/tls, text/template, mime/multipart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:13 +02:00
egutierrez 13e45aa22e docs: cerrar issue 0013
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:01:03 +02:00
egutierrez b5a867ca5a feat: cola de jobs asincrona basada en SQLite (issue 0013)
Implementa el subsistema completo de background jobs para apps Go en el
dominio infra. 9 funciones + 3 tipos + 17 tests, todos pasando.

- Tipos: Job (product), JobQueue (product), JobStatus (sum) con
  JobHandler, EnqueueOption y WorkerOption usando functional options pattern
- job_queue_create: CREATE TABLE + indices + WAL mode
- job_enqueue: INSERT con UUID (github.com/google/uuid), WithPriority/WithScheduledAt/WithMaxAttempts
- job_dequeue: SELECT+UPDATE atomico en transaccion exclusiva, filtro por jobTypes
- job_complete / job_fail: transiciones de estado; fail → dead cuando attempts >= max_attempts
- job_status_summary: pura, formatea conteo de jobs por estado
- job_worker: poll loop bloqueante, context-cancelable, graceful shutdown
- job_worker_pool: N workers con golang.org/x/sync/errgroup
- job_cleanup: DELETE jobs terminales mas viejos que olderThan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:00:44 +02:00
egutierrez e5e8864f50 feat: funciones impuras de testing (http server, db setup/seed, fixtures, env, logs) con tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:00:19 +02:00
egutierrez 9d6b3551a7 feat: funciones puras assert_json_equal y assert_contains_all con tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:00:14 +02:00
egutierrez a38ac74b86 feat: tipos TestServer y TestDB para utilidades de testing en Go core
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 02:00:11 +02:00
egutierrez c226767ebd docs: cerrar issue 0009 — HTTP server functions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 01:58:15 +02:00
egutierrez b074313c01 feat: funciones impuras HTTP — response, parse, logger, router, serve
Seis funciones de servidor HTTP con tests usando httptest:
- HTTPJSONResponse: escribe JSON con Content-Type y status code
- HTTPErrorResponse: escribe HTTPError como JSON estructurado
- HTTPParseBody: decode JSON con limite de bytes y campos estrictos
- HTTPLoggerMiddleware: loguea method/path/status/duration a io.Writer
- HTTPRouter: crea ServeMux con rutas Go 1.22+ (METHOD /path)
- HTTPServe: ListenAndServe con graceful shutdown por contexto

23 tests pasando, solo stdlib net/http.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 01:57:47 +02:00
egutierrez c43322ecc0 feat: funciones puras HTTP — HTTPMiddlewareChain y HTTPCORSMiddleware
- HTTPMiddlewareChain: compone N middlewares preservando el orden (el primero es el mas externo)
- HTTPCORSMiddleware: genera Middleware con headers CORS configurables, maneja OPTIONS preflight con 204

Ambas son puras (sin I/O) y testeadas con httptest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 01:57:39 +02:00
egutierrez 5dbd71307f feat: tipos HTTP — Middleware, Route, HTTPError
Tres tipos Go en el paquete infra para construir servidores HTTP:
- Middleware: funcion que envuelve http.Handler (patron decorator)
- Route: struct con Method, Path y Handler para registrar rutas
- HTTPError: struct con Status, Code y Message para respuestas JSON de error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 01:57:34 +02:00
egutierrez eca52b1329 merge: quick/cmake-path-and-start-script — CMake path fix, sqlite_api start.sh 2026-04-13 01:45:50 +02:00
egutierrez 117c48a783 chore: actualizar path CMake a nueva ubicacion del dashboard + start.sh
cpp/CMakeLists.txt ahora referencia projects/fn_monitoring/apps/
en vez de apps/. Se añade start.sh para lanzar sqlite_api en
background con health check automatico.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 01:45:47 +02:00
egutierrez bc7ef45042 merge: quick/fn-monitoring-project — Proyecto fn_monitoring, mover sqlite_api 2026-04-13 01:36:39 +02:00
egutierrez d79a78a46f feat: proyecto fn_monitoring — agrupa sqlite_api y registry_dashboard
Crea projects/fn_monitoring/ con project.md. Mueve sqlite_api de apps/
a projects/fn_monitoring/apps/. registry_dashboard (repo externo) tambien
se asocia al proyecto via dir_path actualizado. fn index detecta el
project_id automaticamente.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 01:36:32 +02:00
egutierrez 2ef0041862 merge: issue/0008-sqlite-api-web — API REST HTTP read-only para registry.db y operations.db 2026-04-13 01:29:03 +02:00
egutierrez 6f2a8b7847 docs: cerrar issue 0008 — SQLite API Web completado
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 01:24:24 +02:00
egutierrez 678504b123 feat: sqlite_api — API REST HTTP read-only para registry.db y operations.db
App service que expone las bases de datos SQLite del registry como endpoints
HTTP. Solo queries SELECT/PRAGMA, apertura read-only (?mode=ro), timeout 5s,
auto-discovery de operations.db, busqueda FTS5 directa, CORS habilitado.
Puerto default 8484.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 01:24:21 +02:00
egutierrez fe46c69ac0 merge: quick/projects-and-docker-deploy — Docker compose deploy, jupyter fixes, projects structure 2026-04-13 01:17:37 +02:00
egutierrez 2c7edba693 docs: regla projects, estructura projects/vaults, registry.db
Nueva regla projects.md que documenta como agrupar apps, analyses y vaults bajo
un tema comun en projects/{nombre}/. Actualiza INDEX.md con la entrada #15.
Crea directorios projects/ y vaults/ con .gitkeep (contenido real gitignored).
registry.db regenerado con los cambios del indice.
2026-04-13 01:17:25 +02:00
egutierrez 1a0f9b5f86 fix: mejoras en jupyter launcher y kernel startup
write_jupyter_launcher ahora exporta IPYTHONDIR al directorio local .ipython/
para que el kernel cargue el startup correcto cuando se ejecuta desde projects/.
write_jupyter_registry_kernel usa descubrimiento inteligente de FN_REGISTRY_ROOT:
prioriza env var, luego path hardcoded, luego sube desde CWD buscando registry.db.
Esto permite que analyses dentro de projects/ encuentren el registry automaticamente.
2026-04-13 01:17:19 +02:00
egutierrez a8f3c356d2 feat: funcion docker_compose_remote_deploy bash
Funcion bash que despliega un stack Docker Compose en un host remoto via SSH.
Flujo: verificar SSH → git pull → docker-compose pull → docker-compose up -d → listar containers.
Soporta compose files adicionales y retorna JSON con status, containers y duracion.
2026-04-13 01:17:14 +02:00
egutierrez 36f27d109f merge: quick/deploy-infra-projects — Deploy infra, projects y vaults
Sistema de deploy SSH+systemd+rsync con funciones Go y Bash.
Soporte completo de projects y vaults en registry (schema, parser,
store, CLI). Regla deploy para agentes.
2026-04-12 17:30:45 +02:00
egutierrez cc922f04ea docs: regla deploy, template project, gitignore projects/vaults
Añade regla deploy.md con arquitectura SSH+systemd+rsync, workflow
completo para agentes, y referencia a todas las funciones involucradas.
Actualiza INDEX.md con la nueva regla. Añade template project.md para
fn add -k project. Gitignore projects/*/ y vaults/*/ (contenido local,
solo manifests se versionan).
2026-04-12 17:30:01 +02:00
egutierrez 6b8bcd0939 feat: funciones bash de deploy — rsync_deploy y gitea_create_webhook
rsync_deploy sincroniza directorio local a remoto via SSH con
exclusiones estándar (.git, node_modules, *.db, etc.).
gitea_create_webhook crea webhook de push en un repo Gitea para
auto-deploy en cada commit.
2026-04-12 17:29:56 +02:00
egutierrez f21664e052 feat: funciones Go de deploy — systemd, VPS setup, deploy remoto
Nuevas funciones infra para deploy sin Docker: generación de units
systemd (pura), instalación/restart/status de servicios remotos via
SSH, setup inicial de VPS (crear dirs, usuario, permisos), y pipelines
de deploy completo (setup_vps_app, deploy_app_remote). Incluye tipo
DeployConfig con la configuración de deploy por app.
2026-04-12 17:29:52 +02:00
egutierrez 0cc1acb446 feat: subcomando project en CLI con busqueda y listado integrado
Añade cmd/fn/project.go con subcomandos init, list, show y status
para gestionar proyectos desde la CLI. Integra projects en fn search,
fn list y fn show para que aparezcan junto a functions, types y apps.
También añade soporte para vaults en fn show y template project en
fn add -k project.
2026-04-12 17:29:46 +02:00
egutierrez 5992d78941 feat: soporte projects y vaults en registry
Añade tablas projects y vaults a registry.db con FTS5, modelos Go,
parser de project.md y vault.yaml, CRUD completo en store, hashing
determinista, validación, y soporte en el indexer para escanear
projects/{name}/ con sus apps, analysis y vaults anidados.
Migration 010 crea las tablas, triggers FTS5, y columna project_id
en apps/analysis. El indexer preserva records remotos (repo_url) al
reindexar, igual que apps/analysis.
2026-04-12 17:29:41 +02:00
egutierrez 060c5f2ee8 merge: quick/new-bash-go-functions — Nuevas funciones Bash y Go multi-dominio
Batch de funciones nuevas para el registry:
- 12 funciones Bash shell (utilidades de scripting y git)
- 10 funciones Bash infra (instaladores y diagnostico)
- 12 funciones Bash cybersecurity (auditoria y hardening)
- 2 pipelines Bash (inicializacion de proyectos Go)
- 5 funciones Go core (strings y versiones)
- 7 funciones Go infra (gestion SSH config) + tipo SshConfigEntry
- 1 funcion Go shell (extract_script_description)
- 7 funciones Go tui (renderizado y terminal helpers)
2026-04-12 13:55:34 +02:00
egutierrez ebba1e4e02 feat: add Go TUI rendering and terminal helper functions
7 funciones Go del dominio tui: apply_gradient (gradiente de color en texto),
draw_box y draw_separator (renderizado de cajas y separadores con box_chars),
load_ascii_art (carga de arte ASCII desde archivos), normalize_terminal_output
y strip_ansi (limpieza de salida de terminal), read_dir_autocomplete
(autocompletado de rutas de directorio). Incluye box_chars.go como helper
de caracteres Unicode para bordes.
2026-04-12 13:54:54 +02:00
egutierrez 93ef4b8010 feat: add Go extract_script_description function
Funcion Go pura del dominio shell que extrae la descripcion de un script Bash
parseando el header del archivo. Busca comentarios con formato estandar y
retorna la primera linea de descripcion encontrada. Util para indexar scripts
automaticamente.
2026-04-12 13:54:48 +02:00
egutierrez ab7317b0c0 feat: add Go SSH config management functions and type
7 funciones Go del dominio infra para gestion programatica de ~/.ssh/config:
ssh_config_parse (parser de bloques Host/Match), ssh_config_read (lectura del
archivo), ssh_config_find (busqueda por host), ssh_config_add_entry y
ssh_config_remove_entry (CRUD), ssh_config_render (serializacion a texto),
ssh_config_write (escritura atomica). Incluye tipo SshConfigEntry (product type)
y tests unitarios del parser.
2026-04-12 13:54:43 +02:00
egutierrez fce8c48c30 feat: add Go core string and version utility functions
5 funciones Go puras del dominio core: parse_version y compare_versions para
parsing y comparacion semantica de versiones, longest_common_prefix para
encontrar el prefijo comun mas largo entre strings, rel_or_full para devolver
rutas relativas cuando es posible, y split_command_and_arg para separar
comandos de sus argumentos. Todas sin dependencias externas.
2026-04-12 13:54:36 +02:00
egutierrez c383e6464a feat: add bash pipeline functions for Go project initialization
2 pipelines Bash: init_go_module (inicializa un modulo Go con go mod init y
estructura de directorios estandar) e init_go_project (scaffolding completo
de proyecto Go con cmd/, internal/, configs y Makefile). Componen funciones
shell existentes del registry.
2026-04-12 13:54:30 +02:00
egutierrez 7c3f01c9eb feat: add bash cybersecurity audit and hardening functions
12 funciones Bash del dominio cybersecurity: auditoria de red y servicios
(analyze_dns, audit_http_headers, inspect_ssl_cert, list_active_connections,
enumerate_subdomains, geolocate_ip), auditoria de sistema (audit_ssh_config,
check_firewall, detect_suspicious_users), y utilidades crypto (encrypt_file,
generate_password, verify_file_hash). Dominio nuevo en bash/functions/.
2026-04-12 13:54:25 +02:00
egutierrez 194663a379 feat: add bash infra installer and diagnostic functions
10 funciones Bash del dominio infra: instaladores de herramientas de desarrollo
(install_go, install_nodejs, install_pnpm, install_python312, install_uv,
install_volta, install_wails), diagnostico del sistema (analyze_disk_space,
detect_wsl, list_listening_ports). Automatizan la configuracion del entorno
de desarrollo en Linux/WSL.
2026-04-12 13:54:21 +02:00
egutierrez 61d8460149 feat: add bash shell utility functions
12 funciones Bash del dominio shell: utilidades de scripting (bash_log,
bash_colors, bash_check_deps, bash_confirm, bash_handle_error, bash_safe_run),
manipulacion de texto (convert_text_case), estructura de proyectos
(create_project_structure), y operaciones git (git_clean_branches,
git_log_visual, git_push_all_remotes, git_repo_status). Cada una con su
.sh y .md de frontmatter.
2026-04-12 13:54:15 +02:00
egutierrez 61c9042392 merge: issue/0007-dag-engine — Motor de DAGs con CLI, web frontend y SQLite
Reemplaza Dagu con implementacion propia compatible con formato YAML existente.
Incluye parser, validador, topo sort, process manager, execution store SQLite,
scheduler cron, CLI (run/list/status/validate/server) y frontend React/Mantine.
2026-04-12 13:08:26 +02:00
egutierrez dca507ce79 chore: update gitignore files
Agrega prompts/ al gitignore raiz. Actualiza dag_engine/.gitignore
con patrones estandar para Go, frontend y editor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:08:13 +02:00
egutierrez 57c243a81a chore: close issues 0007a-e, update feature flag and sources manifest
Enable dag-engine feature flag, document dagu as analyzed (GPL-3.0,
no code extracted), move all 0007 issues to completed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:06:17 +02:00
egutierrez 741fdcee24 feat: add process manager and execution store types (0007b, 0007c)
Process spawn/wait/kill functions for subprocess management with output
capture, timeout, and process group cleanup. DagRun and DagStepResult
types for SQLite execution persistence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:05:13 +02:00
egutierrez 3136eb862f feat: add DAG core functions — parse, validate, topo sort, resolve env, cron match (0007a, 0007d)
Pure functions for parsing dagu-compatible YAML, validating DAG structure,
topological sorting with parallel levels (Kahn's algorithm), and env variable
resolution. Also adds cron_match for schedule matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:05:05 +02:00
egutierrez d98f517598 merge: quick/issue-0008-sqlite-api-web — SQLite API Web service 2026-04-08 01:29:11 +02:00
egutierrez 85261ce635 feat: add issue 0008 — SQLite API Web service
App Go que expone registry.db y operations.db de cada app como API REST
read-only en localhost:8484, para acceso programático sin SQLite directo.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:10:18 +02:00
egutierrez af9ebd1e0a merge: quick/bigquery-functions — Funciones BigQuery Python, tipo BQClient, comando meta_bigq 2026-04-07 18:45:38 +02:00
egutierrez 982f8d568e feat: add meta_bigq command for Metabase + BigQuery operations
Comando Claude con referencia completa de funciones Metabase y BigQuery,
flujos tipicos y ejemplos de uso combinado.
2026-04-07 18:45:11 +02:00
egutierrez 32c0faac25 chore: add google-cloud-bigquery dependencies
Dependencias del SDK oficial de BigQuery para las funciones Python del registry.
2026-04-07 18:45:06 +02:00
egutierrez 9f5e6791db feat: add BigQuery Python functions and BQClient type
Funciones CRUD completas para BigQuery: auth, datasets, tables, queries,
jobs, routines, load/export. Tipo BQClient como wrapper del SDK oficial.
2026-04-07 18:45:02 +02:00
egutierrez c9f28aa603 merge: quick/mantine-cpp-new-functions — Mantine v9, C++, OSINT refactor, nuevas funciones 2026-04-06 23:47:41 +02:00
egutierrez 73d181f2b0 chore: add Kotlin directory structure, update registry.db and gitignore
Añade estructura inicial kotlin/functions/, actualiza registry.db con todos
los cambios indexados, y ajusta .gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:27 +02:00
egutierrez dede5e00af feat: add Python core and infra functions — PWA, geocoding, POI matching
Nuevas funciones Python: build_guide_prompt, generate_pwa_manifest,
generate_service_worker, match_pois_to_interests (core), nominatim_reverse_geocode,
ollama_chat, overpass_nearby_pois (infra). Incluye tests unitarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:19 +02:00
egutierrez 01042bc23c feat: add bash infra functions — Gitea, Android SDK, Mantine, Capacitor
Nuevas funciones bash: gestión Gitea (create_repo, list_repos, add_collaborator,
push_directory), install_android_sdk, install_mantine, frontend_doctor.
Pipelines: capacitor_build_apk y gitea_init_app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:10 +02:00
egutierrez 817ec9fc36 refactor: reorganize OSINT types — genéricos a core, específicos en cybersecurity
Mueve tipos genéricos (Person, Organization, Location, Email, Phone, Document,
Domain, Event, SocialMedia) de cybersecurity a core. Mantiene en cybersecurity
solo los específicos de seguridad (CryptoWallet, IPAddress, Malware, Vulnerability).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:01 +02:00
egutierrez 97a3c84625 refactor: migrate frontend from shadcn/Tailwind to Mantine v9
Reescribe todos los componentes UI para usar Mantine v9 en lugar de shadcn/Tailwind.
Elimina cn(), CVA, components.json, theme_provider custom y globals.css con Tailwind.
Añade 25+ componentes nuevos (AppShell, AuthForm, DatePickerInput, Dropzone, etc.)
y MantineProvider como wrapper estándar del sistema de temas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:46:44 +02:00
egutierrez 4b2bb6998a feat: add C++ support with ImGui/ImPlot framework and vendor submodules
Añade soporte C++ al registry: vendor submodules (glfw, imgui, implot, tracy),
sistema de build con CMake y toolchains cross-platform, runner C++ en fn CLI,
parser de tests Google Test, y funciones bash para build Linux/Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:46:36 +02:00
egutierrez 0c759c1b66 feat: include registry.db in repo and ignore broken_paths.txt
Share the SQLite registry database so apps/analysis repos can consume
it without needing the full function tree to rebuild.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:55:19 +02:00
egutierrez bf62577e9e merge: quick/docs-params-schema — documentación de params_schema 2026-04-06 00:35:49 +02:00
egutierrez 103cba2a42 docs: documenta params_schema en CLAUDE.md y templates
Actualiza schema rápido, ejemplo FTS5, sección de añadir funciones y los tres
templates (function, pipeline, component) con los campos params/output obligatorios.
2026-04-06 00:35:40 +02:00
egutierrez 882b3a3755 merge: quick/params-schema-composability — params_schema para composabilidad de funciones 2026-04-05 18:45:27 +02:00
egutierrez 988e901066 docs: params/output semántico en 506 funciones para composabilidad
Añade campos params y output al frontmatter YAML de las 506 funciones del registry.
Cada parámetro tiene descripción semántica (qué representa, unidades, rango típico)
y cada función describe qué produce su output. Permite a agentes razonar sobre
cadenas de composición (ej: prices → log_return → sharpe_ratio) sin leer código.
2026-04-05 18:45:16 +02:00
egutierrez bd1bf2b5dc feat: fn check params y fn show muestra params_schema
Nuevo subcomando 'fn check params' lista funciones sin params_schema documentado.
'fn show' ahora muestra el campo Params con el JSON semántico de inputs/outputs.
2026-04-05 18:45:05 +02:00
egutierrez 0da3e530dd feat: add params_schema column for function composability
Nueva columna params_schema en functions con migración 009. Almacena JSON
con descripción semántica de inputs/outputs para que agentes razonen sobre
composabilidad de funciones. Incluye: campo en modelo Go, parsing de params/output
del frontmatter YAML, serialización a JSON, FTS5 rebuild con nueva columna,
hash de contenido actualizado, y warning en indexer cuando faltan params.
2026-04-05 18:45:01 +02:00
egutierrez 7ca3ffb92c merge: quick/unit-tests-e2e-tables — tablas unit_tests y e2e_tests, parser de tests, docs 2026-04-05 18:19:48 +02:00
egutierrez a9cd28b010 chore: añade directorio dev/ con issues y funciones implementadas
Tracking de issues completados (jupyter tools) y funciones implementadas (specs de diseño ya resueltas).
2026-04-05 18:19:36 +02:00
egutierrez 806c819cf7 docs: convenciones de testing y schema unit_tests/e2e_tests
Nuevo docs/testing.md con convenciones de test por lenguaje (Go, Python, Bash con 3 opciones), tablas unit_tests y e2e_tests, consultas FTS5 de ejemplo. Actualiza functions.md y CLAUDE.md con referencia a unit_tests.
2026-04-05 18:19:26 +02:00
egutierrez e6228ea8c0 feat: fn index extrae unit_tests automáticamente
El indexer lee test_file_path de funciones testeadas, parsea los test cases y los inserta en unit_tests. El output de fn index ahora muestra el conteo de unit_tests extraídos.
2026-04-05 18:19:21 +02:00
egutierrez 384a87f8a7 feat: parser automático de test files Go/Python/Bash
Extrae test cases individuales con su código desde archivos _test. Go detecta func TestXxx, Python detecta def test_xxx, Bash soporta tres convenciones: test_xxx(){}, secciones === nombre ===, y comentarios # Test:.
2026-04-05 18:19:17 +02:00
egutierrez fea8fed75d feat: modelos y CRUD para unit_tests y e2e_tests
UnitTest en registry con Insert, GetByFunction, Search FTS5, Purge. E2ETest en fn_operations con Insert, Get, List, UpdateResult, Delete. Ambos con scan helpers y serialización JSON.
2026-04-05 18:19:10 +02:00
egutierrez 512eebf7f4 feat: tablas unit_tests y e2e_tests
Migración 008 en registry.db para unit_tests con FTS5 (tests individuales extraídos de archivos de test). Migración 004 en operations.db para e2e_tests con FTS5 (tests de integración entre funciones dentro de apps).
2026-04-05 18:19:05 +02:00
egutierrez 72f8879f98 merge: quick/bulk-functions-sources-notebook — funciones Go/Python/Bash, tipos, notebook, sources 2026-04-05 17:13:13 +02:00
egutierrez fddc1e7239 fix: pivot_test comparaciones de tipo — sum retorna float64, no int
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:13:03 +02:00
egutierrez 462c1b7a66 chore: dependencias Python, sources manifest, reglas de extracción y comando extract-source
Actualiza pyproject.toml con nuevas dependencias (pdfplumber, python-docx, ebooklib, openpyxl, etc.).
Actualiza sources.yaml con funciones extraídas de repos externos.
Mejora reglas de extracción en sources.md.
Añade comando Claude extract-source para workflow de extracción.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:12:05 +02:00
egutierrez ac05aa489c feat: funciones Bash — install_nbconvert, notebook_to_pdf, export_analysis_pdfs
Infra: install_nbconvert (instala nbconvert+deps), notebook_to_pdf (convierte .ipynb a PDF).
Pipeline: export_analysis_pdfs (exporta todos los notebooks de analysis/ a PDF).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:57 +02:00
egutierrez af1fa129f7 feat: mejoras notebook functions — discover multi-servidor, write batch ops
jupyter_discover: soporte multi-servidor, detección de modo colaborativo mejorada.
jupyter_write: operaciones batch (insert, edit, delete), manejo robusto de Y.js.
jupyter_exec: mejoras en ejecución directa al kernel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:50 +02:00
egutierrez 5a324f6554 feat: funciones Python infra y tipos Python (core, datascience, infra)
Infra: cache_to_file, cache_to_sqlite, http_download_file, http_get_json,
http_post_json, read_file_with_encoding, safe_extract_zip, scan_directory,
setup_logger, normalize_zip_filenames.
Tipos: 30+ tipos core (agent_action, context, task, message, parse_result...),
6 tipos datascience (entity_candidate, extraction_result...), 2 tipos infra.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:43 +02:00
egutierrez 837563c3ba feat: funciones Python datascience, finance, cybersecurity y pipelines
Datascience: aggregate_by_group, deduplicate_entities/relations, detect_drift,
diff_entities/relations, extract_entities/relations_llm, hotness_score, melt,
merge_graphs, pivot, build_entity/relation_schema_prompt.
Finance: avellaneda_stoikov_quotes, generate_gbm_prices, generate_taker_order,
hawkes_intensity + módulo finance.py.
Cybersecurity: envelope_encrypt/decrypt + módulo cybersecurity.py.
Pipelines: extraction_pipeline, monte_carlo_market, run_market_sim.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:32 +02:00
egutierrez 928a3319d5 feat: funciones Python core — parsers, formatters, retry, serialización, LLM utils y más
178 archivos: módulo core.py actualizado + ~80 funciones nuevas con tests.
Incluye: parse_llm_json, extract_text_from_file, retry_with_backoff, circuit_breaker,
from_csv/to_csv, from_jsonl/to_jsonl, html_to_markdown, pdf_to_markdown, docx/epub/excel
converters, cache_decorator, react_loop, task_manager, template rendering, entre otros.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:21 +02:00
egutierrez 53200cbc0d feat: funciones Go — core (cron, join_by_key, validate_struct), datascience (pivot, diff_entities), infra (http, cache, cron_ticker)
Nuevas funciones Go con tests en tres dominios:
- core: parse_cron_expr, next_cron_time, join_by_key, validate_struct_fields + tipo CronSchedule
- datascience: pivot (tabla dinámica), diff_entities (comparación de entidades)
- infra: http_get_json, http_post_json, http_download_file, cache_to_sqlite, cron_ticker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:12 +02:00
egutierrez 51d31a67d1 merge: quick/native-select-search-graph — native select, SearchBar, GraphContainer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:49 +02:00
egutierrez 9b3c57471f feat: componente GraphContainer con sigma.js y graphology
Visualizacion interactiva de grafos con WebGL via sigma.js, estructura de
datos graphology, y layout ForceAtlas2 adaptativo. Soporta grafos dirigidos
multi-edge, leyenda de tipos de nodo, y eventos click/double-click.
Nuevas deps: graphology, sigma, recharts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:34 +02:00
egutierrez 74b4c40f18 feat: componente SearchBar con debounce y clear
Input de busqueda con icono, debounce configurable y boton de limpiar.
Exportado desde index.ts del barrel de UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:29 +02:00
egutierrez 145a6fce8f refactor: migrar Select y SimpleSelect a native HTML select
Select reescrito de @base-ui/react primitives a <select> nativo con wrapper
para mantener la misma API visual (ChevronDown, estilos tema). SimpleSelect
actualizado para usar <select>/<optgroup> directamente sin intermediarios.
Checkbox corregido: import CheckboxIndicator separado reemplazado por
CheckboxPrimitive.Indicator para consistencia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:24 +02:00
egutierrez cd517c705d merge: quick/frontmatter-fixes-new-components-osint — frontmatter fixes, UI components, OSINT types, indexer warnings 2026-04-03 03:24:20 +02:00
egutierrez be71b15afd chore: regla frontend_theming y comandos claude
Nueva regla para usar componentes @fn_library y sistema de temas CSS variables en todos los frontends. Añade directorio de comandos claude.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:52 +02:00
egutierrez 8413db65cc feat: funciones bash audit_registry_paths y validate_registry_paths
Pipeline para auditar file_path del registry contra disco y función shell para validar paths individuales.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:47 +02:00
egutierrez 6ba6507618 feat: tipos OSINT para dominio cybersecurity
13 tipos product (person, email, domain, IP, phone, social_media, organization, location, vulnerability, malware, crypto_wallet, document, event) para modelar entidades de inteligencia de fuentes abiertas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:43 +02:00
egutierrez 820e47304e feat: funciones core frontend — generate_theme_css, get_computed_color, get_theme_tokens
Utilidades TypeScript puras para generación de CSS desde tema, resolución de colores computados y extracción de tokens del sistema de temas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:38 +02:00
egutierrez 35bcb63300 feat: nuevos componentes UI — accordion, avatar, breadcrumb, checkbox, command, dropdown, pagination, popover, radio, sheet, select, switch, textarea, toast
Componentes React accesibles basados en Radix UI con soporte de temas via CSS variables. Incluye barrel export en index.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:32 +02:00
egutierrez 619a56c567 feat: warnings en indexer para file_path inexistentes en disco
Valida post-insert que file_path y test_file_path de funciones y tipos apunten a archivos reales. Reporta warnings sin bloquear el indexado.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:25 +02:00
egutierrez 87e8f33b01 fix: corregir lang y file_path en frontmatter de funciones existentes
Normaliza lang: typescript → ts en funciones frontend y corrige file_path de functions/infra/ → functions/browser/ en funciones CDP. Actualiza referencias cn_typescript_core → cn_ts_core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:20 +02:00
egutierrez f83be63f85 merge: quick/ssh-pass-stubs-embedding-datascience — SSH, pass, stubs, embedding, datascience, jupyter fix 2026-04-02 22:04:29 +02:00
egutierrez c2089e3b0f chore: gitignore .local
Ignora directorio .local para archivos locales de desarrollo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:04:00 +02:00
egutierrez 89e0fb58c6 fix: jupyter_exec usa run_in_executor para execute_cell
Evita bloquear el event loop asyncio ejecutando execute_cell (operación
síncrona con websocket) en un thread executor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:59 +02:00
egutierrez f4d9d09575 feat: módulo embedding — encode, model CRUD, stores sqlvec y usearch
Funciones Python para embeddings: carga/guardado de modelos, encoding de
texto, y almacenamiento/búsqueda vectorial con sqlite-vec y usearch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:57 +02:00
egutierrez 99672a4745 feat: funciones datascience — ops_to_rdf_triples, ops_to_sigma_json, render_sigma_html
Conversión de operations.db a triples RDF y formato sigma.js, más
renderizado HTML standalone con dark theme y ForceAtlas2 layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:51 +02:00
egutierrez fc6681a31a feat: build tags y stubs para clickhouse y duckdb
Añade build tags noclickhouse/noduckdb a las implementaciones reales y
crea stubs que devuelven error para compilar sin las dependencias CGO.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:49 +02:00
egutierrez b698177860 feat: funciones pass para gestión de secretos — get, set, list, delete, generate, sync
Wrappers Bash sobre pass (password-store) para CRUD de secretos, generación
de contraseñas y sincronización con git. Incluye script de test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:44 +02:00
egutierrez 560cbf280e feat: funciones SSH para infra — conn, check, exec, download, upload, tunnel
Conjunto completo de funciones SSH para operaciones remotas: conexión,
verificación de host, ejecución de comandos, transferencia de archivos
(upload/download) y gestión de túneles. Incluye tipo SSHConn y tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:41 +02:00
egutierrez 2b123e0c87 merge: quick/frontend-ui-components-sqlite-open — componentes UI nuevos y mejorados, sqlite_open basePath 2026-04-02 15:32:52 +02:00
egutierrez f164aa592f feat: sqlite_open basePath — resuelve paths relativos desde directorio de config
Nuevo parámetro basePath en SQLiteOpen para resolver paths relativos
contra un directorio base (ej: filepath.Dir del archivo YAML de config)
en lugar del cwd del proceso. basePath vacío mantiene comportamiento anterior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:32:40 +02:00
egutierrez 0be2d7cfe4 feat: mejoras componentes UI — card variants, kpi_card slots, sparkline colors, bar_chart horizontal radius
- card: variantes default/borderless/ghost con ring condicional
- kpi_card: props unit, action, chart y delta con label/suffix personalizable
- sparkline: prop colors para colores por barra en variant bar
- bar_chart: radius condicional según orientación horizontal/vertical

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:32:35 +02:00
egutierrez 55e6ff87a8 feat: componentes data_table y pie_chart — tabla con sorting/pagination y gráfico circular Recharts
Nuevos componentes React/TS en frontend/functions/ui/:
- data_table: tabla de datos con columnas tipadas, sorting, paginación y formato personalizable
- pie_chart: gráfico circular Recharts con tooltips, leyenda y paleta configurable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:32:28 +02:00
egutierrez c7b6e69e06 merge: quick/nordvpn-db-wails-frontend-notebook — NordVPN, DB multi-engine, Wails, frontend React/TS, Jupyter notebook, lorenz_step 2026-04-01 20:56:18 +02:00
egutierrez ddb3fe78cb chore: actualizar deps Go, sources.yaml y funciones infra modificadas
Nuevas dependencias para ClickHouse, DuckDB, Postgres drivers.
Actualizar sources.yaml con funciones extraídas.
Ajustes menores en write_jupyter_launcher, write_mcp_jupyter_config y docker_run_container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:55 +02:00
egutierrez fa1a1b968d docs: regla notebook_collaboration y actualización INDEX
Nueva regla para colaboración en notebooks Jupyter via funciones del registry.
Documenta el flujo discover → read → write → exec y las reglas de uso.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:48 +02:00
egutierrez d77209f663 feat: lorenz_step datascience — paso del atractor de Lorenz
Función pura Go que calcula un paso del sistema de ecuaciones de Lorenz.
Útil para simulaciones de sistemas dinámicos y visualizaciones caóticas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:44 +02:00
egutierrez 8c8b58c3c3 feat: funciones Jupyter notebook Python — discover, read, write, exec, kernel
Funciones Python para interactuar con Jupyter Lab programáticamente:
descubrir instancias, leer/escribir celdas, ejecutar código y gestionar kernels.
Reemplazan MCP jupyter con API REST + WebSocket directa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:39 +02:00
egutierrez 953f598b9b feat: funciones frontend React/TS — componentes UI, hooks Wails, charts y tipos
Componentes React reutilizables: card, dialog, tabs, select, alert, badge, button, input, label,
skeleton, tooltip, progress_bar, page_header, form_field, settings_page, crud_page, analytics_page,
dashboard_layout. Charts: area, bar, line, sparkline, kpi_card, chart_container.
Hooks Wails: use_wails_query, use_wails_mutation, use_wails_stream, use_wails_event, use_animated_canvas.
Funciones core: cn, format_compact, chart_colors, get_series_color, wails_cache, theme_config_to_colors.
Tipos: chart_series, wails_ipc, theme_config, component_variants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:34 +02:00
egutierrez 6deef76427 feat: funciones Wails — scaffold, CRUD bindings, build, eventos y streaming
Funciones Go para crear apps Wails: scaffold estructura, bind CRUD genérico,
build multiplataforma, emit eventos y stream de datos al frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:24 +02:00
egutierrez 47235e702c feat: abstracción DB multi-engine — CRUD genérico y openers para SQLite, Postgres, ClickHouse, DuckDB
Funciones Go con interfaz unificada para operaciones DB: open, close, create_table, exec, query, insert_row, insert_batch.
Openers específicos por engine. Tipo DBConfig para configuración común.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:17 +02:00
egutierrez 2f119478af feat: funciones NordVPN bash y Go — CLI, contenedor Docker y parser de estado
Funciones bash para instalar, conectar, desconectar, estado, IP, ciudades, países y protocolo.
Funciones Go para gestionar contenedor NordVPN (run/start/stop) y parsear estado.
Incluye tipo NordVPNStatus y tests para el parser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:08 +02:00
egutierrez bf1efb2099 feat: externalize apps/analysis to Gitea repos, add analysis table
- Migration 007: repo_url on apps table + analysis table with FTS5
- Analysis struct, parser, CRUD, validation, hash computation
- Selective purge: remote-only apps/analysis preserved across fn index
- CLI: fn app list/clone/pull, fn analysis list/clone/pull
- search/show/list now include analysis results
- Apps removed from git tracking (content lives in Gitea repos)
- .gitkeep for apps/ and analysis/ dirs
- Bash functions: jupyter analysis pipeline, shell utilities
- Browser domain: CDP functions moved from infra to browser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 04:23:51 +02:00
egutierrez 722f29b71b merge: quick/content-hash-sources-infra-functions — content hash, sources, funciones infra/core/PowerShell y app navegador 2026-03-30 14:25:18 +02:00
egutierrez 5d80e5fd57 chore: schema rápido en CLAUDE.md, sync Metabase en CLI, fix main.py
Agrega documentación de schema rápido en CLAUDE.md, regla sources en INDEX.
CLI fn index sincroniza registry.db a directorio Metabase si existe.
fn show muestra campos source_*. Fix import en metabase_registry/main.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:54 +02:00
egutierrez 2fbf4ef62e feat: funciones PowerShell infra — firewall y portproxy
Funciones PowerShell para gestión de red en Windows: win_firewall_add_rule,
win_firewall_remove_rule, win_portproxy_add y win_portproxy_remove.
Útiles para configurar acceso de red en entornos WSL2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:45 +02:00
egutierrez 3c250a9252 feat: app script_navegador y dashboard Metabase
App Go para ejecutar scripts de navegación automatizada usando las
funciones CDP del registry. Incluye script de creación de dashboard
en Metabase para monitoreo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:39 +02:00
egutierrez 9ed0f2e16f feat: cdp_wait_load y mejoras en CDP connect/launch
Nueva función cdp_wait_load para esperar carga completa de página.
CdpConnect ahora soporta host remoto via CdpConnectHost (útil para
WSL2 donde Chrome Windows escucha en IP distinta). Mejoras en
chrome_launch para configuración más flexible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:21 +02:00
egutierrez 1d45f232c6 feat: funciones infra — Docker, deploy, build y health check
Funciones impuras para gestión de contenedores: docker_build_image,
docker_compose_up/down, docker_volume_create/list/remove,
generate_dockerfile, write_dockerfile, go_build_binary, health_check_http,
deploy_app y stop_app. Todas con tests unitarios donde aplica.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:12 +02:00
egutierrez 7e3c1ede79 feat: funciones core — detect_cycle, generate_id, rewrite_rule
Tres funciones puras para el dominio core: detección de ciclos en grafos
dirigidos (DFS), generación de IDs determinísticos, y reescritura de
reglas con pattern matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:00 +02:00
egutierrez ef86aae8fb feat: source attribution para funciones externas
Sistema de extracción de funciones desde repos externos. Agrega campos
source_repo, source_license y source_file en functions y types (migración 006).
Incluye manifest sources/sources.yaml, regla sources.md, parser con campos
de atribución, y template actualizado con los nuevos campos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:23:53 +02:00
egutierrez e349a06ad4 feat: content hash y timestamps inteligentes en registry
Agrega content_hash a functions, types y apps para detectar cambios reales
entre reindexaciones. Los timestamps created_at se preservan si el contenido
no cambió, y updated_at solo se actualiza cuando hay cambios efectivos.
Incluye migración 005, hash.go con SHA256 determinístico, y ajustes en
store/indexer/models para el nuevo flujo de timestamps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:23:45 +02:00
egutierrez 250914c319 merge: quick/chrome-cdp-and-ops-logs — funciones Chrome CDP y logs en operations 2026-03-29 17:31:16 +02:00
egutierrez 615f279323 feat: modelo Log y CRUD en fn_operations
Tipo Log con niveles debug/info/warn/error, source, entity_id y execution_id
opcionales. Migración 003_logs.sql y funciones InsertLog, GetLog, ListLogs
con filtros combinables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:31:03 +02:00
egutierrez 73bf619191 feat: funciones Chrome CDP para automatización de navegador
10 funciones Go en infra/ para controlar Chrome via Chrome DevTools Protocol:
chrome_launch, cdp_connect, cdp_navigate, cdp_evaluate, cdp_screenshot,
cdp_click, cdp_type_text, cdp_wait_element, cdp_get_html, cdp_close.
WebSocket RFC 6455 implementado sin dependencias externas.
Incluye tests de integración con Chrome real.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:30:56 +02:00
egutierrez a4df450d41 fix: findOpsDB falla con error en vez de crear operations.db en la raíz
Antes, si no encontraba operations.db subiendo directorios, hacía
fallback silencioso a ./operations.db — lo que creaba la BD en la raíz
violando la regla de db_locations. Ahora retorna error explícito
indicando que se debe ejecutar fn ops init en el directorio correcto.

También elimina operations.db espuria de la raíz (2 executions de
metabase_registry creadas por el fallback).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:29:47 +02:00
egutierrez 02097ee10f merge: quick/metabase-ops-pipelines — pipelines operativos Metabase y fix permisos SQLite 2026-03-29 00:54:38 +01:00
egutierrez 935c7f30dc docs: documentación completa de metabase_registry
Arquitectura de mounts Docker, tabla de databases, permisos SQLite
(nunca chown, siempre chmod), flujo para app nueva paso a paso,
y referencia a los 3 pipelines relacionados.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:54:28 +01:00
egutierrez 6a05abff03 feat: pipelines Metabase — add ops db, create ops dashboard, fix permissions
Tres pipelines Python para gestionar operations.db en Metabase:
- metabase_add_ops_db: registra la operations.db de una app como database SQLite
- metabase_create_ops_dashboard: genera dashboard operativo con 14 cards (KPIs,
  distribución, executions, assertions) para cualquier app
- metabase_fix_permissions: arregla SQLITE_READONLY_DIRECTORY haciendo chmod
  777/666 sin chown (que se propaga al host via bind mount)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:54:24 +01:00
egutierrez 95bba767bc feat: dashboard apps y mejora layout del dashboard Overview
Dashboard fn-registry Apps con 10 cards: KPIs por lenguaje, dominio,
framework, dependencias y catálogo completo. Cards del Overview
ampliadas a grid de 24 columnas con tamaños más legibles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:54:18 +01:00
egutierrez 26ffba2c03 merge: quick/apps-table-metabase-dashboard — tabla apps, dashboard Metabase, funciones Python 2026-03-29 00:14:21 +01:00
egutierrez 7495c3ea99 feat: dashboard Metabase del registry + regla apps vs functions
Script Python que crea un dashboard en Metabase con 15 cards: KPIs
escalares, distribucion por lenguaje/dominio/kind/pureza, ranking de
funciones mas usadas y complejas, cobertura de tests y tabla cruzada.
Agrega regla apps_vs_functions que establece que codigo reutilizable va
en functions/ y codigo especifico/hardcodeado va en apps/.
2026-03-29 00:14:07 +01:00
egutierrez f570e783fe feat: tabla apps en registry — modelo, parser, indexer y CLI
Agrega soporte completo para indexar aplicaciones del directorio apps/.
Cada app tiene un descriptor app.md con frontmatter YAML que el indexer
recoge automaticamente. Incluye migracion 004, modelo App, ParseAppMD,
ValidateApp, store CRUD con FTS5, y soporte en fn list/search/show.
Crea descriptores app.md para docker_tui, pipeline_launcher y metabase_registry.
2026-03-29 00:13:57 +01:00
egutierrez 95959f713c feat: funciones Python para core, cybersecurity, datascience y finance
Agrega funciones Python reutilizables organizadas por dominio:
- core: composicion funcional (pipe, compose, map, filter, reduce, etc.)
- cybersecurity: analisis de amenazas y puertos
- datascience: estadisticas y deteccion de outliers
- finance: indicadores tecnicos y analisis financiero
2026-03-29 00:13:50 +01:00
egutierrez a4b5651e8c feat: pyrunner mejorado para fn run Python
Refactoriza la ejecucion de funciones Python en fn run. Extrae la logica
a pyrunner.go con soporte para importar dependencias del registry y
ejecutar con el venv del proyecto. Agrega WalCheckpoint en db.go para
que lectores externos vean datos actualizados tras fn index.
2026-03-29 00:13:46 +01:00
egutierrez cd09ddcc76 chore: gitignore completo — __pycache__, .env, venv, node_modules
Añadidos patrones recursivos para Python (__pycache__, .pyc, .venv),
Node (node_modules), y secrets (.env, .env.*) en cualquier subdirectorio.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:25:55 +01:00
egutierrez 3f8d18aa8b merge: quick/fn-run-types-metabase — fn run multi-lenguaje, tipos Go unificados, metabase setup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:45 +01:00
egutierrez 3043518fec docs: CLAUDE.md actualizado con fn run, tipos Go en functions/, bash functions
Documentación de fn run para todos los lenguajes, nueva ubicación de tipos Go,
sección de uso por agentes. Añadidas funciones Bash del registry (shell, infra,
core, pipelines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:30 +01:00
egutierrez 2e5bdacdcf feat: metabase_setup Python, fix list_databases, volumen Docker en init_metabase
Nueva función metabase_setup para setup inicial via API. Fix list_databases
que no extraía data del response wrapper. Pipeline init_metabase soporta
--mb-volumes para montar SQLite como volumen con fix de permisos automático.
Añadido .env a gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:20 +01:00
egutierrez ebbc3bfdab feat: fn run — ejecución multi-lenguaje de funciones y pipelines desde CLI
Nuevo comando que despacha automáticamente según lenguaje: Go pipelines con
go run, Go functions con go test/vet, Python con venv y -m para imports
relativos, Bash directo, TypeScript con tsx del frontend. Resolución por
nombre con desambiguación. Añadido GetFunctionsByName al store y tsx al frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:12 +01:00
egutierrez 4808e16629 fix: stubs shell y tui usan tipos nativos en firmas en vez de Result/Option de core
Result[T] y Option[T] de core no son accesibles desde otros paquetes sin import.
Cambiado a (T, error) y (T, bool) siguiendo la regla de tipos nativos en firmas.
Añadidas dependencias bubbletea/bubbles/lipgloss al go.mod raíz para que tui compile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:06 +01:00
egutierrez 8581d3959a refactor: mover .go de tipos Go a functions/{domain}/ para compilación unificada
Los archivos .go de tipos ahora viven junto a las funciones en functions/{domain}/
(mismo paquete Go), resolviendo errores de compilación por tipos no encontrados
(Option, Pair, Result, etc.). Los .md de metadata permanecen en types/{domain}/
con file_path actualizado a functions/. Se elimina types.go duplicado de infra.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:00 +01:00
egutierrez 5a7b6e3980 merge: quick/frontend-setup-metabase-docs — frontend setup, metabase Go/Py, documentación en registry 2026-03-28 20:33:20 +01:00
egutierrez 6982079b5f chore: gitignore para node_modules, dist y __pycache__
Añade .gitignore en frontend/ y python/ para excluir artefactos
generados. Elimina node_modules/, dist/ y __pycache__/ del tracking.
2026-03-28 20:33:04 +01:00
egutierrez cd7fcd8cca feat: setup frontend con pnpm, vite, react, tailwind y shadcn
Inicializa directorio frontend/ con stack React moderno:
pnpm + vite 8 + react 19 + tailwind v4 + shadcn v4 (base-nova).
Estructura functions/core (TS puro) y functions/ui (componentes React).
El indexer descubre frontend/functions/ y frontend/types/ automáticamente.
Elimina functions/components/ (legacy) y actualiza referencias en
CLAUDE.md y template de componentes.
2026-03-28 20:32:40 +01:00
2731 changed files with 32516 additions and 271473 deletions
+8 -8
View File
@@ -42,10 +42,10 @@ Opcionalmente:
```bash
# Por id
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
# Por dir_path
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE dir_path = '<dir>';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE dir_path = '<dir>';"
```
Si no hay match → reportar y abortar.
@@ -78,8 +78,8 @@ APP_DB="$APP_DIR/operations.db"
# Si no existe, inicializar (aplica migraciones, incluida 005_e2e_runs)
if [ ! -f "$APP_DB" ]; then
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init "$APP_DIR"
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init "$APP_DIR"
fi
# Verificar tabla e2e_runs existe (migracion 005)
@@ -97,7 +97,7 @@ Hay dos caminos:
**Camino A — invocar funcion del registry (preferido):**
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
./fn run e2e_run_checks_go_infra ...
```
@@ -139,15 +139,15 @@ func main() {
Ejecutar con:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
CGO_ENABLED=1 go run -tags fts5 /tmp/run_e2e_<id>.go /tmp/checks.yaml "$APP_DIR"
```
### 5. Eval assertions activas (si la app las tiene)
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db "$APP_DB"
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval --db "$APP_DB"
```
Capturar fallos como warning checks adicionales.
+38 -38
View File
@@ -15,20 +15,20 @@ Eres el agente constructor del fn_registry. Tu rol es crear funciones, tests y t
```bash
# Buscar si ya existe algo similar (OBLIGATORIO antes de crear)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Buscar tipos existentes
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Ver funciones de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
# Ver tipos de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
# Verificar que un ID referenciado existe
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
```
Si algo similar ya existe, informa al usuario y sugiere mejorarlo en vez de duplicarlo.
@@ -39,13 +39,13 @@ Antes de implementar logica desde cero, busca funciones del registry que puedas
```bash
# Buscar funciones reutilizables por lo que hacen (ampliar con OR y prefijos)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
# Ver que retorna y que tipos usa una funcion candidata
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
# Buscar funciones puras del mismo dominio (las mas componibles)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
```
**Criterios de reutilizacion:**
@@ -78,38 +78,38 @@ Esto acelera la construccion y fortalece el grafo de dependencias del registry.
| `bash` | `bash/functions/{domain}/{name}.sh` | `bash/functions/pipelines/{name}.sh` | *(no aplica)* |
| `typescript` | `frontend/functions/{domain}/{name}.ts` | *(no aplica)* | `frontend/types/{domain}/{name}.ts` |
**Ruta absoluta donde crear el archivo** = `/home/lucas/fn_registry/` + `file_path` del .md.
**Ruta absoluta donde crear el archivo** = `$HOME/fn_registry/` + `file_path` del .md.
Ejemplo: si `lang: bash` y `domain: infra`, el archivo va en:
- `/home/lucas/fn_registry/bash/functions/infra/{name}.sh` + `.md`
- **NUNCA** en `/home/lucas/fn_registry/functions/infra/{name}.sh`
- `$HOME/fn_registry/bash/functions/infra/{name}.sh` + `.md`
- **NUNCA** en `$HOME/fn_registry/functions/infra/{name}.sh`
### Estructura detallada
**Go** (carpeta raiz: `functions/` y `types/`)
- Funciones: `/home/lucas/fn_registry/functions/{domain}/{name}.go` + `.md`
- Tests: `/home/lucas/fn_registry/functions/{domain}/{name}_test.go`
- Tipos: `/home/lucas/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `/home/lucas/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
- Pipelines: `/home/lucas/fn_registry/functions/pipelines/{name}.go` + `.md`
- Funciones: `$HOME/fn_registry/functions/{domain}/{name}.go` + `.md`
- Tests: `$HOME/fn_registry/functions/{domain}/{name}_test.go`
- Tipos: `$HOME/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `$HOME/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
- Pipelines: `$HOME/fn_registry/functions/pipelines/{name}.go` + `.md`
- Paquete Go = nombre del directorio (core, finance, datascience, cybersecurity, infra, shell, tui, io)
**Python** (carpeta raiz: `python/`)
- Funciones: `/home/lucas/fn_registry/python/functions/{domain}/{name}.py` + `.md`
- Tests: `/home/lucas/fn_registry/python/functions/{domain}/{name}_test.py`
- Tipos: `/home/lucas/fn_registry/python/types/{domain}/{name}.py` + `.md`
- Pipelines: `/home/lucas/fn_registry/python/functions/pipelines/{name}.py` + `.md`
- Funciones: `$HOME/fn_registry/python/functions/{domain}/{name}.py` + `.md`
- Tests: `$HOME/fn_registry/python/functions/{domain}/{name}_test.py`
- Tipos: `$HOME/fn_registry/python/types/{domain}/{name}.py` + `.md`
- Pipelines: `$HOME/fn_registry/python/functions/pipelines/{name}.py` + `.md`
**Bash** (carpeta raiz: `bash/`)
- Funciones: `/home/lucas/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
- Tests: `/home/lucas/fn_registry/bash/functions/{domain}/{name}_test.sh`
- Pipelines: `/home/lucas/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
- Funciones: `$HOME/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
- Tests: `$HOME/fn_registry/bash/functions/{domain}/{name}_test.sh`
- Pipelines: `$HOME/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
- Tipos: Bash no tiene tipos — usar solo `uses_types` para referenciar tipos de otros lenguajes
**TypeScript** (carpeta raiz: `frontend/`)
- Funciones puras: `/home/lucas/fn_registry/frontend/functions/core/{name}.ts` + `.md`
- Componentes React: `/home/lucas/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
- Funciones puras: `$HOME/fn_registry/frontend/functions/core/{name}.ts` + `.md`
- Componentes React: `$HOME/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
- Tests: junto al archivo, `{name}.test.ts` o `{name}.test.tsx`
- Tipos: `/home/lucas/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
- Tipos: `$HOME/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
---
@@ -591,7 +591,7 @@ Documentar completamente el .md igualmente.
1. **BUSCAR** en registry.db con FTS5 si existe algo similar
2. **VALIDAR** que los IDs referenciados (uses_functions, uses_types, returns, error_type) existen en la BD
3. **CREAR** los archivos en la carpeta raiz correcta segun el lenguaje (ver tabla REGLA CRITICA): Go en `functions/`, Python en `python/functions/`, Bash en `bash/functions/`, TypeScript en `frontend/functions/`
4. **INDEXAR** ejecutando: `cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index`
4. **INDEXAR** ejecutando: `cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index`
5. **VERIFICAR** con: `./fn show {id}` que se indexo correctamente
6. Si hay errores de validacion, corregirlos y re-indexar
@@ -600,10 +600,10 @@ Documentar completamente el .md igualmente.
1. **LEER** la funcion existente (codigo + .md) desde la BD: `sqlite3 registry.db "SELECT code, signature FROM functions WHERE id = '...'"`
2. **CREAR** el archivo de test
3. **EJECUTAR** los tests:
- Go: `cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
- Python: `cd /home/lucas/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
- Go: `cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
- Python: `cd $HOME/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
- TypeScript: desde `frontend/`, ejecutar con el test runner configurado
- Bash: `cd /home/lucas/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
- Bash: `cd $HOME/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
4. **ACTUALIZAR** el .md con `tested: true`, `tests: [...]` y `test_file_path`
5. **RE-INDEXAR** y verificar
@@ -620,19 +620,19 @@ Documentar completamente el .md igualmente.
```bash
# Compilar CLI (necesario si se modifico codigo del CLI)
cd /home/lucas/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
cd $HOME/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
# Indexar registry
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index
# Tests Go de un dominio
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
# Tests Go de todo el registry
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
# Mostrar funcion indexada
cd /home/lucas/fn_registry && ./fn show {id}
cd $HOME/fn_registry && ./fn show {id}
```
### fn run — Ejecutar funciones y pipelines directamente
@@ -640,7 +640,7 @@ cd /home/lucas/fn_registry && ./fn show {id}
Despues de crear/indexar, puedes ejecutar directamente con `fn run`:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
@@ -729,7 +729,7 @@ Peticion: "Crea una funcion que calcule la media de un slice de float64"
### Paso 1: Buscar en BD
```bash
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
```
### Paso 2: Crear archivos
@@ -823,6 +823,6 @@ func TestMean(t *testing.T) {
### Paso 3: Indexar y verificar
```bash
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index
./fn show mean_go_core
```
+68 -68
View File
@@ -35,22 +35,22 @@ Las apps estan indexadas en registry.db con toda la metadata necesaria para ejec
```bash
# Ver todas las apps disponibles
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
# Ver app completa con dependencias y framework
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
# Buscar apps por FTS (nombre, descripcion, tags, documentacion)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Apps de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
# Apps que usan una funcion especifica
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
# Ver documentacion completa de una app
sqlite3 /home/lucas/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
sqlite3 $HOME/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
```
**Campos clave de apps para ejecucion:**
@@ -65,19 +65,19 @@ sqlite3 /home/lucas/fn_registry/registry.db "SELECT documentation, notes FROM ap
```bash
# Ver pipeline/funcion completa
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
# Ver codigo de la funcion
sqlite3 /home/lucas/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
sqlite3 $HOME/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
# Pipelines disponibles (con tag launcher para TUI)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
# Funciones impuras ejecutables directamente
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
# Buscar por FTS
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
```
### Usar contexto de apps para ejecucion inteligente
@@ -98,10 +98,10 @@ Cuando te pidan ejecutar una app, sigue este flujo:
```bash
# Desde la raiz del registry
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Opcion A: Usar el CLI
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
# Opcion B: Copiar template directamente
cp fn_operations/project_template/operations.db apps/{app_name}/operations.db
@@ -221,10 +221,10 @@ Las entities representan los datos concretos del proyecto. Las relations documen
### Crear entities (datos que el pipeline consume o produce)
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Entity de entrada
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ticks" \
--type-ref "tick_go_finance" \
@@ -235,7 +235,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
--metadata '{"pair":"BTCUSDT","exchange":"binance"}'
# Entity de salida
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ohlcv_5m" \
--type-ref "ohlcv_go_finance" \
@@ -249,7 +249,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
### Crear relations (como se conectan entities)
```bash
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation add \
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops relation add \
--db apps/{app_name}/operations.db \
--name "ticks_to_ohlcv" \
--from-entity "{entity_id}" \
@@ -262,13 +262,13 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation add \
```bash
# Listar entities
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
# Listar relations
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
# Ver grafo ASCII
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
@@ -280,7 +280,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/ope
`fn run` despacha automaticamente segun el lenguaje y tipo:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
@@ -318,13 +318,13 @@ Para apps con su propio main.go/main.py/main.sh:
```bash
# Go app
cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
cd $HOME/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
# Python app
cd /home/lucas/fn_registry/apps/{app_name} && python3 main.py [args]
cd $HOME/fn_registry/apps/{app_name} && python3 main.py [args]
# Bash app
cd /home/lucas/fn_registry/apps/{app_name} && bash main.sh [args]
cd $HOME/fn_registry/apps/{app_name} && bash main.sh [args]
```
### Capturar metricas de ejecucion
@@ -340,7 +340,7 @@ Al ejecutar, siempre captura:
```bash
# Ejemplo: ejecutar con captura de tiempo
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
OUTPUT=$(cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
OUTPUT=$(cd $HOME/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -362,7 +362,7 @@ echo "Status: $STATUS | Start: $START | End: $END"
### Via CLI
```bash
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
--db apps/{app_name}/operations.db \
--pipeline-id "tick_to_ohlcv_go_finance" \
--relation-id "{relation_id}" \
@@ -396,16 +396,16 @@ sqlite3 apps/{app_name}/operations.db "INSERT INTO executions (id, pipeline_id,
```bash
# Listar todas
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
# Por pipeline
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
# Por status
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
# Detalle de una ejecucion
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
```
---
@@ -441,12 +441,12 @@ Si hay assertions definidas sobre las entities afectadas, evaluarlas para verifi
```bash
# Evaluar assertions de una entity
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID"
# Evaluar Y reaccionar (actualiza status de entities, crea proposals si hay fallos criticos)
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID" \
--react
@@ -467,10 +467,10 @@ Cuando el usuario pide ejecutar algo que aun no tiene app:
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
mkdir -p $HOME/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: go
@@ -490,7 +490,7 @@ dir_path: "apps/{app_name}"
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
@@ -499,7 +499,7 @@ build/
GIEOF
# 4. Inicializar modulo Go
cd /home/lucas/fn_registry/apps/{app_name}
cd $HOME/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# 5. Crear main.go minimo
@@ -523,8 +523,8 @@ func main() {
GOEOF
# 6. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
# 7. Indexar en registry.db
./fn index
@@ -534,10 +534,10 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
mkdir -p $HOME/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: py
@@ -557,7 +557,7 @@ dir_path: "apps/{app_name}"
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
@@ -565,7 +565,7 @@ __pycache__/
GIEOF
# 4. Crear main.py
cat > /home/lucas/fn_registry/apps/{app_name}/main.py << 'PYEOF'
cat > $HOME/fn_registry/apps/{app_name}/main.py << 'PYEOF'
"""Pipeline executor."""
import sys
import time
@@ -584,8 +584,8 @@ if __name__ == "__main__":
PYEOF
# 5. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
@@ -595,10 +595,10 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
mkdir -p $HOME/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: bash
@@ -618,14 +618,14 @@ dir_path: "apps/{app_name}"
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
GIEOF
# 4. Crear main.sh
cat > /home/lucas/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
cat > $HOME/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
#!/usr/bin/env bash
# Pipeline executor: {app_name}
set -euo pipefail
@@ -650,11 +650,11 @@ main() {
main "$@"
SHEOF
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
chmod +x $HOME/fn_registry/apps/{app_name}/main.sh
# 5. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
@@ -669,7 +669,7 @@ Este patron captura todo lo necesario para registrar la ejecucion:
### Go
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
APP_DIR="$HOME/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
RELATION_ID="{relation_id}" # vacio si no aplica
@@ -689,8 +689,8 @@ else
fi
# Registrar ejecucion
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
@@ -704,7 +704,7 @@ rm -f "$STDOUT_FILE" "$STDERR_FILE"
### Python
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
APP_DIR="$HOME/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -716,8 +716,8 @@ END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "{pipeline_id}" \
--status "$STATUS" \
@@ -728,7 +728,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
### Bash
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
APP_DIR="$HOME/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
@@ -741,8 +741,8 @@ END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
@@ -758,10 +758,10 @@ Antes de ejecutar, verifica que los snapshots de tipos en operations.db estan al
```bash
# Verificar snapshots
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Actualizar si estan desactualizados
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
```
---
@@ -800,7 +800,7 @@ Crea una proposal cuando detectes:
### Como crear proposals
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Proposal para nueva funcion
./fn proposal add \
@@ -840,7 +840,7 @@ Cuando la proposal viene de un fallo o anomalia en una ejecucion, incluye la evi
```bash
# Obtener el ID de la ejecucion que evidencia el problema
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list \
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list \
--db apps/{app_name}/operations.db --status failure
# Incluir evidencia en la descripcion
@@ -858,19 +858,19 @@ Usa el contexto de la tabla apps para comparar y detectar patrones:
```bash
# Ver que funciones usan las apps — detectar patrones comunes
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
# Ver funciones mas usadas por apps (candidatas a mejora)
sqlite3 /home/lucas/fn_registry/registry.db "
sqlite3 $HOME/fn_registry/registry.db "
SELECT f.value as func_id, COUNT(*) as uso
FROM apps, json_each(apps.uses_functions) f
GROUP BY f.value ORDER BY uso DESC;"
# Ver apps que NO tienen funciones del registry (candidatas a extraccion)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
# Ver si ya existe una proposal para algo similar
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
```
### Flujo de deteccion al ejecutar
+5 -5
View File
@@ -43,12 +43,12 @@ APP_ID="<input>"
RUN_ID="<input>"
# dir_path desde registry
DIR_PATH=$(sqlite3 /home/lucas/fn_registry/registry.db \
DIR_PATH=$(sqlite3 $HOME/fn_registry/registry.db \
"SELECT dir_path FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
APP_ID=$(sqlite3 /home/lucas/fn_registry/registry.db \
APP_ID=$(sqlite3 $HOME/fn_registry/registry.db \
"SELECT id FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
APP_DB="/home/lucas/fn_registry/$DIR_PATH/operations.db"
APP_DB="$HOME/fn_registry/$DIR_PATH/operations.db"
[ ! -f "$APP_DB" ] && APP_DB="/tmp/$(basename $DIR_PATH)_e2e_runs.db"
# Sanity check
@@ -93,7 +93,7 @@ Por cada fallo:
Antes de crear proposal, verificar que no haya una identica abierta:
```bash
sqlite3 /home/lucas/fn_registry/registry.db "
sqlite3 $HOME/fn_registry/registry.db "
SELECT id FROM proposals
WHERE status = 'pending'
AND target_id = '$APP_ID'
@@ -139,7 +139,7 @@ Sugerencia generica en `description` (NO codigo concreto, solo direccion):
Si la misma assertion/check ha disparado proposal mas de 3 veces en los ultimos 30 dias, marcar `priority` (campo extendido si existe, si no, anotar en `description: '[REINCIDENTE x4]'`).
```bash
sqlite3 /home/lucas/fn_registry/registry.db "
sqlite3 $HOME/fn_registry/registry.db "
SELECT COUNT(*) FROM proposals
WHERE target_id = '$APP_ID'
AND title LIKE '%::$CHECK_ID%'
+14 -14
View File
@@ -30,14 +30,14 @@ 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:
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/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/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
git -C $HOME/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.
@@ -49,24 +49,24 @@ Antes de arrancar el bucle, comprobar:
```bash
# 1. Migration 006_task_runs.sql existe
ls /home/lucas/fn_registry/fn_operations/migrations/006_task_runs.sql 2>/dev/null \
ls $HOME/fn_registry/fn_operations/migrations/006_task_runs.sql 2>/dev/null \
|| { echo "ABORT: migration 006_task_runs.sql ausente. Aplicar issue 0069 paso 1 antes."; exit 2; }
# 2. Subagentes fn-* presentes
for a in fn-constructor fn-executor fn-recopilador fn-analizador fn-mejorador; do
test -f /home/lucas/fn_registry/.claude/agents/$a/SKILL.md \
test -f $HOME/fn_registry/.claude/agents/$a/SKILL.md \
|| { echo "ABORT: subagente $a ausente"; exit 2; }
done
# 3. master local up-to-date con origin (worktree se creara desde master)
git -C /home/lucas/fn_registry fetch origin master --quiet
LOCAL=$(git -C /home/lucas/fn_registry rev-parse master)
REMOTE=$(git -C /home/lucas/fn_registry rev-parse origin/master)
git -C $HOME/fn_registry fetch origin master --quiet
LOCAL=$(git -C $HOME/fn_registry rev-parse master)
REMOTE=$(git -C $HOME/fn_registry rev-parse origin/master)
test "$LOCAL" = "$REMOTE" \
|| { echo "ABORT: master local desincronizado con origin. git pull antes."; exit 2; }
# 4. Branch auto/<issue> NO existe ya (ni local ni en worktrees)
git -C /home/lucas/fn_registry rev-parse --verify "auto/${ISSUE_ID}" >/dev/null 2>&1 \
git -C $HOME/fn_registry rev-parse --verify "auto/${ISSUE_ID}" >/dev/null 2>&1 \
&& { echo "ABORT: branch auto/${ISSUE_ID} ya existe. Limpiar antes (git branch -D + worktree remove)."; exit 2; }
# 5. gh CLI autenticado (necesario para PR draft al converger)
@@ -116,7 +116,7 @@ BRANCH="auto/${ISSUE_ID}"
TASK_RUN_ID="task_$(openssl rand -hex 8)"
STARTED_AT=$(date +%s)
WT_ROOT="/tmp/fn_orq_${ISSUE_ID}_${STARTED_AT}"
REPO="/home/lucas/fn_registry"
REPO="$HOME/fn_registry"
# Crear worktree aislado desde master (no toca el principal)
git -C "$REPO" worktree add -b "$BRANCH" "$WT_ROOT" master \
@@ -187,13 +187,13 @@ while iter < max_iterations and elapsed < max_minutes:
Usar `Agent` tool con `subagent_type` correcto. Prompt **autocontenido** (paths absolutos, IDs, criterio exito).
**CRITICO**: pasar `WT_ROOT` (worktree path) en cada prompt y exigir al subagente trabajar dentro de el. Subagentes NO deben tocar el repo principal `/home/lucas/fn_registry/`.
**CRITICO**: pasar `WT_ROOT` (worktree path) en cada prompt y exigir al subagente trabajar dentro de el. Subagentes NO deben tocar el repo principal `$HOME/fn_registry/`.
Patron prompt:
```
Working dir: <WT_ROOT> # NO /home/lucas/fn_registry
Working dir: <WT_ROOT> # NO $HOME/fn_registry
Branch: auto/<issue_id>
Repo principal (solo lectura para registry.db): /home/lucas/fn_registry
Repo principal (solo lectura para registry.db): $HOME/fn_registry
...
```
@@ -346,7 +346,7 @@ Cada `progress_json` entry:
|---|---|---|
| `task_runs` no existe | migration 006 no aplicada | abortar pre-condicion 1 |
| `worktree add` falla con "already exists" | branch o dir previo no limpiado | `git worktree prune` + `git branch -D auto/<id>`, reintentar |
| Subagente toca `/home/lucas/fn_registry/` en vez de worktree | prompt sin `WT_ROOT` explicito | rebriefing con working dir explicito |
| Subagente toca `$HOME/fn_registry/` en vez de worktree | prompt sin `WT_ROOT` explicito | rebriefing con working dir explicito |
| `master` desincronizado con origin | falta `git pull` | abortar pre-condicion 3 |
| Loop infinito (mismo fail siempre) | watchdog ausente o desactivado | watchdog OBLIGATORIO, no skipear |
| Subagente devuelve output ambiguo | prompt insuficiente | rebriefing con paths/IDs explicitos |
+20 -20
View File
@@ -40,10 +40,10 @@ apps/{app_name}/
```bash
# Listar todas las apps
ls -d /home/lucas/fn_registry/apps/*/
ls -d $HOME/fn_registry/apps/*/
# Verificar que cada app tiene app.md
for app in /home/lucas/fn_registry/apps/*/; do
for app in $HOME/fn_registry/apps/*/; do
name=$(basename "$app")
echo "=== $name ==="
[ -f "$app/app.md" ] && echo " app.md: OK" || echo " app.md: FALTA"
@@ -82,8 +82,8 @@ sqlite3 "$APP_DB" "SELECT * FROM schema_migrations ORDER BY version;" 2>/dev/nul
**Si faltan tablas**, aplicar migraciones:
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
```
### 3. Integridad de Entities
@@ -96,7 +96,7 @@ sqlite3 "$APP_DB" "SELECT id, name, type_ref, status, domain, source FROM entiti
# Validar que type_ref existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT type_ref FROM entities;" | while read ref; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
if [ -z "$EXISTS" ]; then
echo "ERROR: type_ref '$ref' no existe en registry.db"
fi
@@ -129,7 +129,7 @@ sqlite3 "$APP_DB" "SELECT r.id, r.name, r.to_entity FROM relations r WHERE r.to_
# Validar que 'via' referencia una funcion/pipeline del registry
sqlite3 "$APP_DB" "SELECT DISTINCT via FROM relations WHERE via != '';" | while read via; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
if [ -z "$EXISTS" ]; then
echo "ERROR: relation.via '$via' no existe en registry.db"
fi
@@ -156,7 +156,7 @@ sqlite3 "$APP_DB" "SELECT id, pipeline_id, status, started_at, duration_ms, reco
# Validar que pipeline_id existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT pipeline_id FROM executions;" | while read pid; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: pipeline_id '$pid' no existe en registry.db"
fi
@@ -230,7 +230,7 @@ sqlite3 "$APP_DB" "SELECT id, version, lang, algebraic, snapped_at FROM types_sn
# Comparar con registry.db — detectar snapshots desactualizados
sqlite3 "$APP_DB" "SELECT id, version FROM types_snapshot;" | while IFS='|' read id ver; do
REG_VER=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
REG_VER=$(sqlite3 $HOME/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
if [ -z "$REG_VER" ]; then
echo "WARN: snapshot '$id' ya no existe en registry.db"
elif [ "$ver" != "$REG_VER" ]; then
@@ -252,14 +252,14 @@ done
```bash
# Verificar que la app esta en registry.db
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
# Verificar que uses_functions del app.md coincide con lo indexado
sqlite3 /home/lucas/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
sqlite3 $HOME/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
# Verificar que todas las funciones referenciadas existen
sqlite3 /home/lucas/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
sqlite3 $HOME/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: app usa funcion '$fid' que no existe en registry"
fi
@@ -273,7 +273,7 @@ done
Patron para auditar TODAS las apps de una vez:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
echo "========================================="
echo "AUDITORIA DE APPS — fn-recopilador"
@@ -327,7 +327,7 @@ for app_dir in apps/*/; do
[ "$ERROR_LOGS" -gt 0 ] 2>/dev/null && echo " [WARN] $ERROR_LOGS logs de error"
# 9. App indexada en registry.db
INDEXED=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
INDEXED=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
[ -n "$INDEXED" ] && echo " [OK] Indexada en registry.db" || echo " [WARN] NO indexada en registry.db"
done
@@ -393,25 +393,25 @@ echo "========================================="
El recopilador puede sugerir o ejecutar estas reparaciones:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Aplicar migraciones faltantes
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
# Actualizar snapshot desactualizado
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
# Verificar snapshots
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Evaluar assertions pendientes
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
# Re-indexar para que la app aparezca en registry.db
./fn index
# Ver grafo de la app (util para diagnostico visual)
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
+13 -13
View File
@@ -38,13 +38,13 @@ Antes de crear nada, recopilar contexto:
```bash
# Buscar funciones relevantes por descripcion
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
# Buscar apps similares
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Verificar que el nombre no esta tomado
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
```
4. **Presentar plan al usuario** antes de ejecutar:
@@ -79,7 +79,7 @@ Usar el Agent tool con `subagent_type: "fn-constructor"` pasando:
Despues de que fn-constructor termine, verificar que todo se indexo:
```bash
cd /home/lucas/fn_registry && ./fn index
cd $HOME/fn_registry && ./fn index
# Verificar cada funcion creada
./fn show {id_de_cada_funcion}
```
@@ -91,7 +91,7 @@ cd /home/lucas/fn_registry && ./fn index
### Estructura base (todos los lenguajes)
```bash
mkdir -p /home/lucas/fn_registry/apps/{app_name}
mkdir -p $HOME/fn_registry/apps/{app_name}
```
### app.md (OBLIGATORIO — siempre primero)
@@ -143,7 +143,7 @@ build/
**Go (CLI/TUI):**
```bash
cd /home/lucas/fn_registry/apps/{app_name}
cd $HOME/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# Crear main.go, app/, config/, views/ segun necesidad
```
@@ -151,7 +151,7 @@ go mod init fn_registry/apps/{app_name}
**Go (Wails — desktop con UI):**
```bash
# Usar scaffold del registry
cd /home/lucas/fn_registry
cd $HOME/fn_registry
./fn run scaffold_wails_app -- --name {app_name} --dir apps/{app_name}
```
@@ -165,20 +165,20 @@ cd /home/lucas/fn_registry
```bash
# Crear main.sh con source a funciones del registry
# Pattern: source "$REGISTRY_ROOT/bash/functions/{domain}/{func}.sh"
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
chmod +x $HOME/fn_registry/apps/{app_name}/main.sh
```
### Inicializar operations.db
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
cd $HOME/fn_registry
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
```
### Indexar en registry.db
```bash
cd /home/lucas/fn_registry && ./fn index
cd $HOME/fn_registry && ./fn index
# Verificar
sqlite3 registry.db "SELECT id, name, lang, domain FROM apps WHERE name = '{app_name}';"
```
@@ -241,7 +241,7 @@ Usar el Agent tool con `subagent_type: "gitea"` pasando:
```bash
# 1. Crear repo en Gitea (via API)
# 2. Inicializar git en la app
cd /home/lucas/fn_registry/apps/{app_name}
cd $HOME/fn_registry/apps/{app_name}
git init
git add -A
git commit -m "Initial commit: {app_name} — {descripcion}"
@@ -256,7 +256,7 @@ git push -u origin master
**Despues de publicar**, actualizar el `repo_url` en app.md y re-indexar:
```bash
cd /home/lucas/fn_registry && ./fn index
cd $HOME/fn_registry && ./fn index
```
---
+59 -22
View File
@@ -1,37 +1,74 @@
# /compile — Compila app C++ y la copia al escritorio de Windows
---
description: "Compila app del registry (C++ o Wails Go), copia el .exe a Desktop/apps/<app>/ y relanza en Windows. Wrapper sobre compile_cpp_app o compile_wails_app segun framework declarado en app.md."
---
Wrapper sobre el pipeline `compile_cpp_app_bash_pipelines`. Toda la lógica vive en el registry (resolver app desde CWD/arg, cross-compile MinGW, copiar exe + DLLs + assets/ + enrichers/ + runtime/ a `/mnt/c/Users/lucas/Desktop/apps/<app>/`, taskkill previo, preservar `local_files/`).
# /compile — Compila app C++ o Wails y la copia al escritorio de Windows
Wrapper sobre 2 pipelines del registry segun el framework:
- **C++ (imgui / cmake)** → `compile_cpp_app_bash_pipelines`. Cross-compile MinGW + assets/enrichers/runtime + taskkill, NO relanza.
- **Wails Go (matrix_client_pc, matrix_admin_panel, etc.)** → `compile_wails_app_bash_pipelines`. `wails build -platform windows/amd64` con `-tags goolm` si E2EE + taskkill + **RELANZA** la app tras copy.
Toda la logica vive en el registry (resolver app desde CWD/arg, build, deploy con preservacion de `local_files/`).
## Dispatch
```bash
cd /home/lucas/fn_registry
./fn run compile_cpp_app "$ARGUMENTS"
cd $HOME/fn_registry
# Detecta framework via wails.json o CMakeLists.txt en el dir del app
APP="$ARGUMENTS"
RESOLVED=$(bash -c '
source bash/functions/infra/resolve_cpp_app_dir.sh
resolve_cpp_app_dir "'"$APP"'"
' 2>/dev/null) || true
APP_DIR="$(echo "$RESOLVED" | cut -f2)"
if [ -n "$APP_DIR" ] && [ -f "$APP_DIR/wails.json" ]; then
./fn run compile_wails_app "$ARGUMENTS"
elif [ -n "$APP_DIR" ] && [ -f "$APP_DIR/CMakeLists.txt" ]; then
./fn run compile_cpp_app "$ARGUMENTS"
else
echo "ERROR: no se detecto framework (falta wails.json o CMakeLists.txt en $APP_DIR)" >&2
exit 1
fi
```
## Argumento
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`).
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`, `matrix_client_pc`).
- Sin argumento: deduce desde `pwd` si estás dentro de `cpp/apps/<X>/` o `projects/*/apps/<X>/`.
- Si no se puede deducir y no se pasa argumento, el pipeline lista las apps disponibles en stderr y aborta.
- Sin argumento: deduce desde `pwd` si estas dentro de `cpp/apps/<X>/`, `apps/<X>/` o `projects/*/apps/<X>/`.
- Si no se puede deducir y no se pasa argumento, lista las apps disponibles en stderr y aborta.
## Qué hace el pipeline
## Que hace el pipeline (C++)
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>` desde arg o CWD.
2. Verifica `CMakeLists.txt` en el dir resuelto.
3. `build_cpp_windows_bash_infra <app>` — cross-compila el target específico con `cpp/build/windows/` (configura toolchain `mingw-w64.cmake` la primera vez).
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>`.
2. Verifica `CMakeLists.txt`.
3. `build_cpp_windows_bash_infra <app>` — cross-compila con MinGW.
4. `deploy_cpp_exe_to_windows_bash_infra <app> <dir>`:
- `taskkill.exe /IM <app>.exe /F` (pre-autorizado).
- Copia `<app>.exe` + DLLs al top-level de `Desktop/apps/<app>/`.
- rsync `cpp/build/windows/apps/<app>/assets/` `Desktop/apps/<app>/assets/`.
- rsync `<app_dir>/enrichers/``assets/enrichers/` si existe.
- Si `app.md` declara `python_runtime: true`, regenera `runtime/` con `tools/freeze_python_runtime.sh` y rsync a `assets/runtime/`.
- Copia `gx-cli`/`gx-cli.exe` si existen.
- **NUNCA** toca `local_files/` (estado del usuario).
5. Imprime `ls -lh` del `.exe` final.
- `taskkill.exe /IM <app>.exe /F`.
- Copia `<app>.exe` + DLLs.
- rsync `assets/`, `enrichers/`, `runtime/` (si aplica).
- Preserva `local_files/`.
- **NO** relanza.
## Que hace el pipeline (Wails)
1. `resolve_cpp_app_dir_bash_infra` (reusado — sirve para Wails apps tambien).
2. Verifica `wails.json` + `go.mod`.
3. Detecta `-tags goolm` automaticamente (grep `matrix_crypto_init` en `app.md` o `build:tags` en `wails.json`).
4. `wails build -platform windows/amd64 [-tags goolm]`.
5. `deploy_wails_exe_to_windows_bash_infra <app> <dir>`:
- `taskkill.exe /IM <app>.exe /F`.
- Copia `<app>.exe` (+ `appicon.ico` si existe).
- **Relanza** via `cmd.exe /c start "" <app>.exe`.
- Preserva `local_files/`.
## Notas
- Solo target Windows hoy. Android / Linux quedan fuera (Linux ya lo da `cpp/build/`).
- Solo target Windows hoy. Linux ya lo da `wails build` / `cpp/build/` nativo.
- Variables override-ables: `BUILD_WIN`, `WIN_DESKTOP_APPS`, `FN_REGISTRY_ROOT`.
- Si la app no está registrada en `cpp/CMakeLists.txt`, `cmake --build --target <app>` falla. Registrar siguiendo `.claude/rules/cpp_apps.md` §5.
- Para tocar la lógica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,deploy_cpp_exe_to_windows,compile_cpp_app}.sh`, no este wrapper.
- Si la app C++ no esta registrada en `cpp/CMakeLists.txt`, el build falla — registrar siguiendo `.claude/rules/cpp_apps.md` §5.
- Si la app Wails falla build con `no required module provides package`, correr `go mod tidy` en el dir del app primero.
- Para tocar la logica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,build_cpp_windows,deploy_{cpp,wails}_exe_to_windows,compile_{cpp,wails}_app}.sh`, no este wrapper.
+6 -6
View File
@@ -23,8 +23,8 @@ Si `$ARGUMENTS` no empieza por `modify`, es create. Si trae `<name>`, lo usas co
### 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
test -d "$HOME/fn_registry/apps/<name>" \
|| ls $HOME/fn_registry/projects/*/apps/<name> 2>/dev/null
```
Si existe en cualquier ubicacion: **abortar** y sugerir `/cpp-app modify <name>`. NO sobreescribir.
@@ -42,7 +42,7 @@ Regla dura `cpp_apps.md`: description + icon.phosphor + icon.accent SIEMPRE junt
5. **icon.phosphor** glyph name. Antes de preguntar, ofrece busqueda:
```bash
ls /home/lucas/fn_registry/sources/phosphor-core/assets/fill/ | grep -i "<keyword>"
ls $HOME/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):
@@ -122,7 +122,7 @@ Mostrar bloque YAML completo del `app.md` que se va a generar + flags del scaffo
Una vez confirmado:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# 1. Scaffolder
./fn run init_cpp_app <name> \
@@ -178,7 +178,7 @@ cd /home/lucas/fn_registry
```bash
# Buscar apps/<name>/ o projects/*/apps/<name>/
sqlite3 /home/lucas/fn_registry/registry.db \
sqlite3 $HOME/fn_registry/registry.db \
"SELECT id, dir_path FROM apps WHERE name='<name>' AND lang='cpp';"
```
@@ -211,7 +211,7 @@ Para cada cambio: usa `Edit` sobre los archivos correspondientes. NUNCA `Write`
```bash
# Siempre
cd /home/lucas/fn_registry && ./fn index
cd $HOME/fn_registry && ./fn index
# Si toco icon.* -> regenerar appicon
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
+12 -12
View File
@@ -38,19 +38,19 @@ Consultar `registry.db` para encontrar funciones existentes relevantes y evitar
```bash
# Buscar funciones similares por nombre y descripcion (OBLIGATORIO — usar multiples terminos)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
# Buscar tipos relacionados
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Funciones del dominio objetivo
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
# Tipos del dominio objetivo
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
# Funciones que podrian componerse (misma firma de retorno)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
```
**Clasificar resultados en:**
@@ -103,7 +103,7 @@ Para cada batch del plan, lanzar agentes `fn-constructor` **en paralelo** (un ag
Usar el Agent tool con `subagent_type: "fn-constructor"` pasando un prompt completo con:
```
Crea la siguiente funcion para el registry fn_registry en /home/lucas/fn_registry:
Crea la siguiente funcion para el registry fn_registry en $HOME/fn_registry:
Funcion: {nombre}
Kind: {kind}
@@ -149,7 +149,7 @@ Despues de que TODOS los fn-constructor terminen:
```bash
# Indexar todo de una vez
cd /home/lucas/fn_registry && ./fn index
cd $HOME/fn_registry && ./fn index
```
Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
@@ -166,7 +166,7 @@ Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
```bash
# Verificar cada funcion creada
cd /home/lucas/fn_registry
cd $HOME/fn_registry
./fn show {id_de_cada_funcion}
# Verificar que no hay funciones sin params_schema
@@ -178,7 +178,7 @@ cd /home/lucas/fn_registry
Para cada funcion con tests, ejecutar:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Go
CGO_ENABLED=1 go test -tags fts5 -v -run TestNombreDelTest ./functions/{domain}/
@@ -197,13 +197,13 @@ bash bash/functions/{domain}/{nombre}_test.sh
```bash
# Verificar que todas las funciones nuevas estan en la BD
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
# Verificar que los tests estan indexados
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
# Verificar dependencias
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
sqlite3 $HOME/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
```
### 6.4 Si algo fallo
+3 -3
View File
@@ -45,7 +45,7 @@ Antes de escribir nada, repasar la conversacion y juntar:
2. **Cambios concretos** desde git:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
git status --short
git diff --stat
git log --since="6 hours ago" --oneline
@@ -70,7 +70,7 @@ Si el material es solo conversacion exploratoria sin artefactos tocados, ir dire
Para cada artefacto identificado, localizar su `.md` consultando `registry.db`:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
# Funcion / tipo
sqlite3 registry.db "SELECT id, file_path FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:NAME* OR description:NAME*');"
@@ -180,7 +180,7 @@ Para cada `.md` identificado:
Si los cambios de la sesion incluyen creacion de funciones/tipos/apps/projects/analysis/vaults o modificacion de frontmatter:
```bash
cd /home/lucas/fn_registry && ./fn index
cd $HOME/fn_registry && ./fn index
```
Y verificar:
+1 -1
View File
@@ -17,7 +17,7 @@ Suite ya instalada en `cpp/vendor/imgui_test_engine/`. Integracion en framework:
### 1. Resolver app y directorio
```bash
ROOT=/home/lucas/fn_registry
ROOT=$HOME/fn_registry
ARGS="$ARGUMENTS"
APP_ARG="${ARGS%% *}" # primera palabra
FLOW_DESC="${ARGS#* }" # resto (puede coincidir con APP_ARG si solo hay una palabra)
+1 -1
View File
@@ -17,7 +17,7 @@ Wrapper sobre `append_diary_entry_bash_infra`. La función del registry maneja t
2. **Llamar la función del registry**:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
source bash/functions/infra/append_diary_entry.sh
append_diary_entry "<TITULO>" "$(cat <<'EOF'
<CUERPO>
+1 -1
View File
@@ -50,7 +50,7 @@ Issue 0085 fase autocompleta. Reemplaza el flujo manual de "veo un patron, decid
### 1. AUDIT — ¿estoy siendo registrado?
```bash
ROOT="/home/lucas/fn_registry"
ROOT="$HOME/fn_registry"
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
# Pre-condiciones
+1 -1
View File
@@ -3,7 +3,7 @@
Wrapper sobre el pipeline `full_git_pull_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
./fn run full_git_pull_bash_pipelines
```
+1 -1
View File
@@ -3,7 +3,7 @@
Wrapper sobre el pipeline `full_git_push_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
```bash
cd /home/lucas/fn_registry
cd "${FN_REGISTRY_ROOT:-$HOME/fn_registry}"
./fn run full_git_push_bash_pipelines "$ARGUMENTS"
```
+1 -1
View File
@@ -3,7 +3,7 @@
Wrapper sobre el pipeline `init_cpp_app_bash_pipelines`. Genera la estructura canonica que cumple `cpp/PATTERNS.md` y `.claude/rules/cpp_apps.md` (main.cpp con `cfg.about/log/panels`, sin `app_menubar` manual, dockspace via framework), registra la app en `cpp/CMakeLists.txt`, crea repo Gitea `dataforge/<name>` y ejecuta `fn index`.
```bash
cd /home/lucas/fn_registry
cd $HOME/fn_registry
./fn run init_cpp_app $ARGUMENTS
```
+1 -1
View File
@@ -17,7 +17,7 @@ Si vacio: detectar app desde `pwd` (si estas dentro de `apps/<X>/` o `projects/*
### 1. Resolver app objetivo
```bash
ROOT=/home/lucas/fn_registry
ROOT=$HOME/fn_registry
ARG="$ARGUMENTS"
if [ -z "$ARG" ]; then
+1
View File
@@ -38,3 +38,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 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. |
| 34 | [dod_quality.md](dod_quality.md) | DoD Quality Triada: Mecanica + Cobertura (golden + edge + error path con evidencia ejecutable) + Vida util validada (>=7 dias uso real). Cierra anti-criterios contra checkbox vago. Aplica a `dev/flows/` y issues user-facing. |
+1 -1
View File
@@ -16,7 +16,7 @@
```bash
# 1. Agente trabaja en worktree del repo padre
cd /home/lucas/fn_registry/worktrees/<slug>
cd $HOME/fn_registry/worktrees/<slug>
# 2. Scaffold la app via pipeline canonico
./fn run init_cpp_app <name> # apps C++
+131
View File
@@ -0,0 +1,131 @@
# DoD Quality Triada
**Definition of Done no es un checkbox que se marca a mano. Es un contrato de calidad con 3 capas obligatorias + evidencia ejecutable + uso real >=7 dias.**
Aplica a todos los `dev/flows/` y, por extension, a issues que cierran capabilities user-facing (`dev/issues/`). El registry mismo (funciones puras, tipos) queda exento: su DoD vive en sus tests unitarios.
---
## Por que existe esta regla
El antipatron a eliminar: "tarea hecha porque pase los tests una vez". Despues:
- El flow funciona en `home-wsl` pero falla en `pc-aurgi`.
- El error path declarado nunca se ejercito y cuando ocurre en produccion no esta manejado.
- El dashboard de observabilidad lleva 30 dias sin abrirse.
- El proceso muere cada noche y nadie lo ve hasta que el operador intenta usarlo.
- El approval flow se salta porque "para test es mas comodo".
Resultado: deuda invisible. Cada flow "done" se rompe al primer uso real, el operador pierde confianza en el sistema, y el bucle reactivo no detecta nada porque la telemetria esta verde (los tests sintenticos pasan).
DoD Quality Triada cambia las reglas: cerrar = probar comportamiento + sobrevivir uso real, no = compilar verde.
---
## Las 3 capas
### Capa 1: Mecanica (pre-requisito, NO es DoD por si misma)
Compilar verde, tests verdes, indexado limpio, `fn doctor` verde, `uses_functions` sin drift.
**Regla**: la mecanica NO basta. Es la base para empezar a probar comportamiento. Si te quedas aqui, el flow no esta hecho.
### Capa 2: Cobertura de comportamiento
Cada escenario relevante con prueba ejecutable y assert material. NO smoke "el comando no peto". Minimo:
- **1 golden path** — el caso feliz documentado con assert sobre output concreto.
- **>=2 edge cases** — inputs limite, estados raros, condiciones de borde.
- **>=1 error path** — fallo provocado intencionalmente, manejado y observable (sin crash, sin silent-fail).
Formato canonico (tabla en `## Definition of Done` del flow/issue):
```markdown
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|---|---|---|---|
| Golden: <desc> | unit / e2e | `<cmd>` | <output concreto> |
| Edge 1: <desc> | unit / e2e | `<cmd>` | <comportamiento concreto> |
| Error 1: <desc> | e2e | `<cmd que rompe>` | <fallo manejado, no crash> |
```
Cuando aplique, cada fila genera un `e2e_check` en el `app.md` correspondiente (issue 0068). `fn-analizador` los corre periodicamente y deja entry en `e2e_runs`.
### Capa 3: Vida util validada
El flow no esta hecho hasta que sobrevive **uso real durante >=7 dias** sin romperse silenciosamente. Cada metrica con umbral medible y dashboard observable.
Formato canonico:
```markdown
| Metrica | Umbral | Donde se observa | Ventana |
|---|---|---|---|
| <metrica 1> | `>=N` | `<dashboard URL / app panel>` | 7 dias |
| crashes | `0` | `journalctl -u <unit>` | 7 dias |
| huecos audit chain | `0` | `cmd: <verify>` | continuo |
```
Reglas:
- Metricas NO se auto-reportan; las lee el operador del dashboard real.
- Si el dashboard no existe o no se ha abierto en 30 dias, el item se invalida.
- Crashes del proceso = 0, huecos en audit = 0, error_rate < umbral declarado.
### Capa transversal: User-facing reforzado
- Surface concreta NO BD ni log (UI app, room Matrix, dashboard, archivo en vault).
- Usage real: humano usa en su PC, su contexto, >=N veces variadas en >=7 dias.
- Variado: >=3 capabilities/casos distintos (no solo "abre dashboard y mira").
- Onboarding: parrafo en `## Notas` que explica como usar la cosa sin leer el flow.
- Latencia medida (no declarada).
---
## Reglas duras para marcar `status: done`
`/flow done` (y por extension cierres de issues user-facing) DEBE rechazar el cierre si:
1. Falta cualquiera de las 3 capas (mecanica + cobertura + vida).
2. Cobertura tiene <1 golden, <2 edge, o <1 error path con evidencia.
3. Vida util tiene tabla vacia o sin dashboard observable real.
4. User-facing usage real <7 dias o <N usos declarados.
5. Cualquier anti-criterio marcado como cierto.
6. `## Notas` sin parrafo onboarding.
7. Algun item de DoD sin comando/URL/log query asociado — solo texto.
Hoy parte de esta validacion es manual (revision humana del operador). La validacion programatica vive en `audit_dod_schema_go_infra` (issue 0114) + `fn doctor dod` y se ampliara hasta cubrir las 3 capas (TBD).
---
## Antipatrones (invalidan la DoD aunque los checkboxes esten verdes)
| Antipatron | Por que es malo | Sustituir por |
|---|---|---|
| Marcar `done` porque pasa una vez | Tarea "hecha" se rompe al primer uso real | Capa 3: >=7 dias de uso real |
| Checkbox sin evidencia ejecutable | DoD se convierte en placebo | Cada item con `cmd:` / URL / log query |
| Test que solo verifica camino feliz | El error path es donde se pierden datos | Capa 2: >=1 error path ejercitado |
| Observabilidad declarada pero dashboard no abierto en 30 dias | Telemetria muerta = ceguera | Capa 3: dashboard real, operador lo abre |
| "Repetible 3 veces consecutivas" con BD efimera | No prueba sobre datos reales acumulados | Capa 3: PC real del operador, datos vivos |
| Approval saltado en algun camino | Security gate roto pero invisible | Anti-criterio explicito: `audit_log` lo prueba |
| Error path manejado solo "en teoria" | Cuando ocurra en produccion el manejo no existe | Capa 2: entry real en `e2e_runs` o audit |
| Solo-en-mi-PC | Falla en otra maquina del operador | Anti-criterio explicito, probar >=2 PCs |
| Self-test que retorna `pass` sin asserts materiales | False positive sistemico | Asserts sobre output concreto, no exit-0 |
| Silent-fail (proceso muere sin alerta) | Operador no se entera hasta intentar usar | Capa 3: crashes=0 + alerta visible |
---
## Relacion con otras reglas
- [[e2e_validation]] — los escenarios de Capa 2 cuando aplican a apps se materializan como `e2e_checks` en `app.md`. `fn-analizador` (fase 4 del bucle reactivo) los corre.
- [[registry_calls]] — la evidencia de uso (`call_monitor.calls`) alimenta los umbrales de Capa 3.
- [[function_growth_and_self_docs]] — cada funcion del registry tiene su propio contrato self-doc (Ejemplo + Cuando usarla + Gotchas). DoD del flow NO sustituye al self-doc de la funcion; lo complementa para el nivel sistema.
- [[autonomous_loop]] — `fn-orquestador` autonomo NO puede marcar `done` sin que se cumplan las 3 capas. Su criterio de convergencia incluye DoD Quality.
- [[apps_tbd]] — TBD garantiza master desplegable; DoD garantiza que lo desplegado funciona en uso real.
---
## TL;DR
1. **Mecanica** = compilar verde (pre-requisito, NO suficiente).
2. **Cobertura** = golden + >=2 edge + >=1 error path con evidencia ejecutable.
3. **Vida util** = >=7 dias de uso real sin romper silenciosamente, dashboard observable abierto.
4. **User-facing reforzado** = humano usa en PC real, >=N veces variadas.
5. **Anti-criterios** invalidan la DoD aunque todo este verde.
6. Sin evidencia ejecutable (cmd/URL/log), NO es DoD: es deseo.
+35
View File
@@ -0,0 +1,35 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_registry_mcp.sh" },
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_fn_match.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Bash|Edit|Write|MultiEdit|mcp__registry__.*",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_call_monitor.sh" }
]
},
{
"matcher": "Edit|Write|MultiEdit|mcp__registry__fn_create_function",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_capability_tag_gate.sh" }
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_capabilities_inject.sh" },
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/scripts/hook_registry_first_reminder.sh" }
]
}
]
}
}
+1
View File
@@ -69,6 +69,7 @@ temp/
# C++ build artifacts
cpp/build/
/build/
# OS
.DS_Store
+7
View File
@@ -1,22 +1,29 @@
[submodule "cpp/vendor/imgui"]
path = cpp/vendor/imgui
url = https://github.com/ocornut/imgui.git
shallow = true
branch = docking
[submodule "cpp/vendor/implot"]
path = cpp/vendor/implot
url = https://github.com/epezent/implot.git
shallow = true
[submodule "cpp/vendor/tracy"]
path = cpp/vendor/tracy
url = https://github.com/wolfpld/tracy.git
shallow = true
[submodule "cpp/vendor/glfw"]
path = cpp/vendor/glfw
url = https://github.com/glfw/glfw.git
shallow = true
[submodule "cpp/vendor/implot3d"]
path = cpp/vendor/implot3d
url = https://github.com/brenocq/implot3d.git
shallow = true
[submodule "cpp/vendor/sdl3"]
path = cpp/vendor/sdl3
url = https://github.com/libsdl-org/SDL.git
shallow = true
[submodule "emsdk"]
path = emsdk
url = https://github.com/emscripten-core/emsdk.git
shallow = true
+4
View File
@@ -3,6 +3,10 @@
"registry": {
"command": "./apps/registry_mcp/registry_mcp",
"args": ["--enable-run", "--enable-write"]
},
"jupyter": {
"command": "bash",
"args": ["/home/enmanuel/fn_registry/bash/functions/infra/jupyter_mcp_serve.sh"]
}
}
}
+22
View File
@@ -0,0 +1,22 @@
[2026-05-22 23:18:14.872] [INFO] app start: Agents Dashboard
[2026-05-22 23:24:12.811] [INFO] app start: Agents Dashboard
[2026-05-22 23:24:14.628] [INFO] [connect] testing https://agents.organic-machine.com...
[2026-05-22 23:24:14.758] [INFO] [connect] OK
[2026-05-22 23:24:14.765] [INFO] [db] base_url saved
[2026-05-22 23:24:14.765] [INFO] [fetch_agents] starting
[2026-05-22 23:24:14.766] [INFO] [fetch_agents] requesting https://agents.organic-machine.com/agents
[2026-05-22 23:24:14.903] [INFO] [fetch_agents] response status=200 err= body_len=3146
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] parsed 11 rows
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] done
[2026-05-22 23:24:14.910] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:27:07.469] [INFO] app start: Agents Dashboard
[2026-05-22 23:27:08.242] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:27:36.670] [INFO] app start: Agents Dashboard
[2026-05-22 23:27:37.446] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
[2026-05-22 23:28:07.068] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:03.025] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:38.605] [INFO] app start: Agents Dashboard
[2026-05-22 23:30:48.267] [INFO] app start: Agents Dashboard
[2026-05-22 23:40:58.931] [INFO] app start: Agents Dashboard
[2026-05-22 23:41:16.455] [INFO] app start: Agents Dashboard
[2026-05-22 23:42:35.646] [INFO] app start: Agents Dashboard
-18
View File
@@ -1,18 +0,0 @@
# Build output
dag_engine
*.exe
# Frontend build
frontend/dist/
frontend/node_modules/
# Go
vendor/
# Editor
.idea/
.vscode/
*.swp
# OS
.DS_Store
-360
View File
@@ -1,360 +0,0 @@
# dag_engine — Guia de uso
Motor de DAGs propio del fn_registry. **Scheduler oficial** del ecosistema (issue 0007a-e + flow 0001). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**.
---
## 1. Donde viven los DAGs
| Path | Que |
|---|---|
| `apps/dag_engine/dags_migrated/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). |
| `apps/dag_engine/dags_migrated/archive/` | DAGs deshabilitados (no se cargan por el scheduler). |
Por defecto el systemd unit apunta a `apps/dag_engine/dags_migrated/`. Para usar otro dir, edita `~/.config/systemd/user/dag_engine.service`:
```ini
ExecStart=/home/lucas/fn_registry/apps/dag_engine/dag_engine server \
--port 8090 \
--dags-dir /home/lucas/fn_registry/apps/dag_engine/dags_migrated \
--db /home/lucas/fn_registry/apps/dag_engine/dag_engine.db \
--scheduler
```
Y reload + restart:
```bash
systemctl --user daemon-reload
systemctl --user restart dag_engine.service
```
---
## 2. Anadir un DAG nuevo (workflow)
### Paso a paso
1. **Crear YAML** en `apps/dag_engine/dags_migrated/<nombre>.yaml` (ver formato en seccion 3).
2. **Validar** sin ejecutar:
```bash
./apps/dag_engine/dag_engine validate apps/dag_engine/dags_migrated/<nombre>.yaml
```
Salida esperada: `Validation: PASS`. Si falla, ver seccion 5 (diagnostico).
3. **Probar ejecucion manual** una vez:
```bash
./apps/dag_engine/dag_engine run apps/dag_engine/dags_migrated/<nombre>.yaml
```
4. **Recargar scheduler** (toma el YAML automaticamente al iterar el dir):
```bash
systemctl --user restart dag_engine.service
journalctl --user-unit dag_engine.service -n 30 --no-pager
```
Busca la linea `[scheduler] ticker started for <nombre> (<cron>)` en los logs.
5. **Verificar en frontend**:
- C++ ImGui: panel `DAGs` muestra el nuevo DAG. Pulsa `Refresh` si no aparece.
- Web: `http://localhost:8090`.
### Disparo manual desde curl o frontend
```bash
curl -X POST http://127.0.0.1:8090/api/dags/<nombre>/run
```
Devuelve `{"dag":"<nombre>","run_id":"...","status":"accepted"}` y dispara el WS broadcast — los frontends ven la run en `<1s`.
---
## 3. Formato YAML
Formato YAML propio de dag_engine. Schema: `name`, `description`, `schedule`, `env`, `tags`, `working_dir`, `steps[]`, `handlers` (alias `handler_on`).
### Ejemplo completo
```yaml
name: my_pipeline
description: "Pipeline diario que importa CSV y actualiza Metabase."
group: finanzas # opcional, agrupa DAGs en listados
type: graph # opcional: graph (default) | chain
tags: [daily, csv, metabase] # opcional, filtros en la UI
# Variables de entorno (heredadas por todos los steps).
env:
- DATA_DIR: /home/lucas/data
- SLACK_HOOK: ${SLACK_HOOK_PROD} # interpolacion de ENV del host
# Cron schedule. Puede ser string o lista.
schedule:
- "0 9 * * *" # 09:00 todos los dias
- "0 21 * * 5" # 21:00 viernes (segundo trigger)
# Working dir + shell por defecto para todos los steps.
working_dir: /home/lucas/fn_registry
shell: /bin/bash
timeout_sec: 1800 # 30 min para todo el DAG
steps:
- name: ingest
description: "Descarga CSV."
command: ./bash/functions/pipelines/ingest_csv.sh
timeout_sec: 300 # 5 min para este step
env:
- SOURCE_URL: https://example.com/data.csv
- name: transform
description: "Limpieza y agregacion."
script: |
#!/usr/bin/env python3
import pandas as pd
df = pd.read_csv("$DATA_DIR/raw.csv")
df.to_parquet("$DATA_DIR/clean.parquet")
depends: [ingest] # debe terminar OK antes
retry_policy:
limit: 2 # reintentos en caso de fallo
interval_sec: 60
- name: load_metabase
command: ./bash/functions/metabase/refresh_dashboard.sh
depends: [transform]
continue_on:
failure: true # no aborta el DAG aunque falle
- name: notify
command: ./bash/functions/io/slack_send.sh "pipeline OK"
depends: [load_metabase]
# Hooks de ciclo de vida.
handler_on:
success: ./bash/functions/io/notify_success.sh
failure: ./bash/functions/io/notify_failure.sh
exit: ./bash/functions/io/cleanup.sh
```
### Campos del DAG (top-level)
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador unico. Debe matchear el filename sin extension. |
| `description` | string | "" | Texto libre, aparece en la UI. |
| `group` | string | "" | Agrupa DAGs en listados. |
| `type` | string | `""` (graph) | `graph` o `chain`. graph = grafo dirigido por `depends`. chain = ejecucion secuencial implicita. |
| `working_dir` | string | cwd del server | Path absoluto desde donde lanzar los steps. |
| `shell` | string | `/bin/sh` | Shell para `command:`. |
| `env` | list/map | [] | Variables de entorno DAG-wide. |
| `schedule` | string/list | "" | Cron expressions (5 campos: min hour dom mon dow). Vacio = solo manual. |
| `steps` | list | (obligatorio) | Pasos del DAG (>=1). |
| `handler_on` | map | null | Hooks `init/success/failure/exit`. Alias: `handlers`. |
| `tags` | list[string] | [] | Filtros en la UI. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout global del DAG en segundos. |
### Campos de cada step
| Campo | Tipo | Default | Que |
|---|---|---|---|
| `name` | string | (obligatorio) | Identificador del step dentro del DAG. |
| `id` | string | "" | Override del id auto-generado. |
| `description` | string | "" | Texto libre. |
| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). |
| `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. |
| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run <id> <args...>` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. |
| `args` | list[string] | [] | Args extra para `command` o para la `function`. |
| `shell` | string | hereda | Override del shell. |
| `dir` / `working_dir` | string | hereda | Working dir para este step. |
| `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. |
| `env` | list/map | hereda | Env del step (sobrescribe el del DAG). |
| `continue_on.failure` | bool | false | Si true, el DAG sigue aunque este step falle. |
| `continue_on.skipped` | bool | false | Si true, dependientes corren aunque este step quede skipped. |
| `retry_policy.limit` | int | 0 | Reintentos. |
| `retry_policy.interval_sec` | int | 0 | Segundos entre reintentos. |
| `timeout_sec` | int | 0 (sin timeout) | Timeout del step. |
| `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). |
| `tags` | list[string] | [] | Tags por step (UI). |
### Function steps (coherencia con el registry)
Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end.
```yaml
steps:
- name: audit_capabilities
function: audit_capability_groups_go_infra
args: ["--json"]
description: "Audita drift entre tags de capability group y paginas madre"
```
Ventajas vs `command: ./fn run ...`:
- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable).
- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente).
- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step.
- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error.
- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`.
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
Ejemplo completo: `apps/dag_engine/dags_migrated/daily-registry-audit.yaml`.
### Cron schedule
5 campos clasicos: `min hour dom mon dow`. Ejemplos:
| Expresion | Significado |
|---|---|
| `0 9 * * *` | Todos los dias a las 09:00 |
| `*/15 * * * *` | Cada 15 minutos |
| `0 */6 * * *` | Cada 6 horas en punto |
| `0 9 * * 1-5` | 09:00 lunes-viernes |
| `0 21 * * 5` | 21:00 viernes |
Multiples cron en `schedule:` -> el DAG dispara por cada uno.
---
## 4. Comandos CLI
```bash
./dag_engine run <path.yaml> # ejecuta un DAG ad-hoc
./dag_engine list [dir] # lista DAGs con schedule + ultimo status
./dag_engine status [dag_name] # historial de ejecuciones
./dag_engine validate <path.yaml> # parse + validate (no ejecuta)
./dag_engine server # arranca HTTP + WS hub + frontend embebido
```
Flags del `server`:
| Flag | Default | Que |
|---|---|---|
| `--port` | 8090 | Puerto HTTP. |
| `--dags-dir` | `apps/dag_engine/dags_migrated` (via systemd unit) | Dir scaneado para YAMLs. |
| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. |
| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. |
---
## 5. Que hacer si algo falla
### 5.1. El DAG no aparece en la UI
**Sintoma:** anadiste un YAML pero `GET /api/dags` no lo lista.
| Causa | Diagnostico | Fix |
|---|---|---|
| YAML invalido | `./dag_engine validate <path>` muestra el error. | Corregir segun el mensaje (campo desconocido, indentacion, type wrong). |
| Filename con extension fuera de `.yaml`/`.yml` | `ls apps/dag_engine/dags_migrated/` | Renombrar a `.yaml`. |
| El servidor apunta a otro dir | `systemctl --user cat dag_engine.service` -> ver `--dags-dir`. | Ajustar unit y `daemon-reload + restart`. |
| Cache UI antiguo | C++: pulsa `Refresh`. Web: `Ctrl+F5`. | — |
### 5.2. Validation: FAIL
`validate` muestra `parse error: ...` o `Validation: FAIL`. Causas tipicas:
| Mensaje | Causa | Fix |
|---|---|---|
| `yaml unmarshal: ...` | Sintaxis YAML rota (indentacion, tab vs espacios). | Usar 2 espacios consistentes. Validar online con `yamllint`. |
| `dag_parse: step[N]: name is required` | Step sin `name:`. | Anadir `name:`. |
| `dag_parse: step[N]: command or script required` | Step sin `command` ni `script`. | Anadir uno de los dos. |
| `cycle detected: A -> B -> A` | `depends` forma ciclo. | Romper la dependencia o convertir uno de los nodos en step distinto. |
| `unknown depends: <step>` | `depends:` referencia un step inexistente. | Comprobar nombres exactos (case-sensitive). |
| `invalid cron: <expr>` | Cron mal formado (4 o 6 campos en vez de 5). | Verificar `0 9 * * *` (5 campos). |
### 5.3. El DAG corre pero un step falla
**Sintoma:** `status: failed` en la UI.
1. Abre `DAG Detail` y haz doble-click en el run rojo -> `Run Detail`.
2. Expande el step que fallo (CollapsingHeader). Muestra `stdout` + `stderr`.
3. Errores tipicos:
| stderr | Causa | Fix |
|---|---|---|
| `command not found` | `command:` apunta a un binario fuera de `PATH`. | Path absoluto o setear `env: [PATH: ...]`. |
| `permission denied` | Script sin `chmod +x`. | `chmod +x <script>` (o usar `bash <script>`). |
| `no such file or directory` | `working_dir:` mal o ruta relativa rota. | Path absoluto en `working_dir:`. |
| Timeout | Step duro mas que `timeout_sec`. | Subir el limite o partir el step. |
| Exit 137 / OOM kill | Out-of-memory. | Reducir batch o anadir swap. |
### 5.4. El scheduler no dispara
**Sintoma:** Hay `schedule:` valido pero el DAG no corre solo.
1. Verifica que el server arranco con `--scheduler`:
```bash
systemctl --user cat dag_engine.service | grep scheduler
```
2. Logs:
```bash
journalctl --user-unit dag_engine.service -n 50 --no-pager | grep -E "scheduler|ticker"
```
Debes ver `[scheduler] ticker started for <name> (<cron>), next: <ISO8601>`.
3. Si `next:` es muy lejano (ej. en una semana) y necesitas probar -> dispara manual:
```bash
curl -X POST http://127.0.0.1:8090/api/dags/<name>/run
```
4. Hora del sistema descalibrada:
```bash
timedatectl status
```
Si difiere de la hora real, `sudo timedatectl set-ntp true`.
### 5.5. El frontend C++ no conecta WS
**Sintoma:** Panel `Live (WS)` muestra `disconnected`.
| Causa | Fix |
|---|---|
| Servidor caido | `systemctl --user status dag_engine.service`, `restart` si `inactive`. |
| Puerto cambiado | El cliente apunta a `127.0.0.1:8090` por codigo (constante `g_ws_port`). Reedificar si cambiaste el puerto del server. |
| Firewall Windows -> WSL | WSL2 expone `localhost`, normalmente OK. Si falla: `wsl --shutdown` y reabrir. |
### 5.6. Cleanup de runs viejos
`dag_runs` y `dag_step_results` crecen sin limite. Para limpiar:
```bash
sqlite3 apps/dag_engine/dag_engine.db <<'SQL'
DELETE FROM dag_step_results WHERE run_id IN (
SELECT id FROM dag_runs WHERE started_at < datetime('now', '-30 days')
);
DELETE FROM dag_runs WHERE started_at < datetime('now', '-30 days');
VACUUM;
SQL
```
### 5.7. Restaurar desde backup
Si rompes `dags_migrated/`, recupera desde el snapshot de `backup_all_bash_pipelines` (BACKUP_ROOT por defecto `~/backups/fn_registry`):
```bash
cp ~/backups/fn_registry/registry/daily.0/dags_migrated/*.yaml \
apps/dag_engine/dags_migrated/ 2>/dev/null || \
git checkout HEAD -- apps/dag_engine/dags_migrated/
systemctl --user restart dag_engine.service
```
---
## 6. Endpoints HTTP
| Metodo | Path | Que |
|---|---|---|
| GET | `/api/dags` | Lista DAGs + last_run + last_runs[5]. |
| GET | `/api/dags/{name}` | Detalle + validation. |
| POST | `/api/dags/{name}/run` | Dispara ejecucion (trigger=`api`). Devuelve `run_id`. |
| GET | `/api/runs` | Historial. Query: `dag`, `limit`, `offset`. |
| GET | `/api/runs/{id}` | Detalle de un run + sus step_results. |
| GET | `/api/ws/dagruns` | WebSocket. Snapshot + deltas en vivo (issue 0095). |
| GET | `/api/scheduler/status` | Tickers activos. |
| POST | `/api/scheduler/start` | Arranca scheduler (si no estaba). |
| POST | `/api/scheduler/stop` | Para scheduler. |
---
## 7. Referencias
- Schema parser: `functions/core/dag_parse.go` (frontmatter en `dag_parse_go_core`).
- Validator: `functions/core/dag_validate.go` (`dag_validate_go_core`).
- Topo sort: `functions/core/dag_topo_sort.go` (`dag_topo_sort_go_core`).
- Cron: `functions/core/parse_cron_expr.go` + `next_cron_time.go`.
- Frontend C++: `cpp/apps/dag_engine_ui/` (issue 0095).
- WS hub: `apps/dag_engine/events.go`.
- dag_engine es el scheduler oficial del ecosistema. Single-binary Go + SQLite, sin dependencias externas.
-55
View File
@@ -1,55 +0,0 @@
package main
import (
"io/fs"
"net/http"
)
// RegisterAPI sets up all HTTP routes on the given mux.
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))
mux.HandleFunc("POST /api/dags/{name}/run", handleRunDag(executor))
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))
}
}
// spaHandler serves static files from the embedded FS, falling back to index.html
// for unknown paths (SPA client-side routing).
func spaHandler(fsys fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Try to serve the file directly.
path := r.URL.Path
if path == "/" {
path = "index.html"
} else {
path = path[1:] // strip leading /
}
if _, err := fs.Stat(fsys, path); err != nil {
// File not found — serve index.html for SPA routing.
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
-143
View File
@@ -1,143 +0,0 @@
---
name: dag_engine
lang: go
domain: infra
version: 0.1.0
description: "Motor de ejecucion de DAGs del fn_registry: CLI + servidor HTTP + scheduler cron. Schema YAML propio con `function:` para invocar funciones del registry (`fn run <id>`) y `command:` para shell. Historial en SQLite. Scheduler oficial del ecosistema."
tags: [service, dag, workflow, scheduler, web, cron]
uses_functions:
- dag_parse_go_core
- dag_validate_go_core
- dag_topo_sort_go_core
- dag_resolve_env_go_core
- parse_cron_expr_go_core
- next_cron_time_go_core
- cron_ticker_go_infra
- find_go_core
- process_spawn_go_infra
- process_wait_go_infra
uses_types:
- dag_definition_go_core
- dag_step_go_core
- dag_validation_result_go_core
- cron_schedule_go_core
- process_handle_go_infra
- process_result_go_infra
- DagRun_go_infra
- DagStepResult_go_infra
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
CLI + servidor web en un unico binario:
```
dag-engine run <path.yaml> # ejecuta un DAG desde terminal
dag-engine list [dir] # lista DAGs con schedule y estado
dag-engine status [dag_name] # historial de ejecuciones
dag-engine validate <path.yaml> # valida sin ejecutar
dag-engine server # arranca HTTP + frontend web
```
### Backend (Go)
- `net/http` con `ServeMux` (Go 1.22+ pattern routing)
- SQLite via `go-sqlite3` para historial de runs
- Executor: parse -> validate -> topo_sort -> spawn/wait por nivel -> store
- Scheduler: cron_ticker por cada DAG con schedule
### Frontend (Vite + React + Mantine)
- DagList: tabla de DAGs con schedule, tags, ultimo status
- DagDetail: metadata + "Run Now" + historial
- RunDetail: timeline de steps con stdout/stderr expandible
### Storage
SQLite `dag_engine.db`:
- `dag_runs`: id, dag_name, status, trigger, started_at, finished_at, error
- `dag_step_results`: id, run_id, step_name, status, exit_code, stdout, stderr, duration_ms
### Build
```bash
cd frontend && pnpm install && pnpm build
cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
```
### Uso
```bash
# CLI
./dag-engine run apps/dag_engine/dags_migrated/fn_backup.yaml
./dag-engine list apps/dag_engine/dags_migrated/
# 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
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.
-34
View File
@@ -1,34 +0,0 @@
package main
import (
"flag"
"os"
"path/filepath"
)
// Config holds the runtime configuration for the DAG engine.
type Config struct {
Port int
DagsDir string
DBPath string
AutoScheduler bool
}
// DefaultConfig returns sensible defaults.
func DefaultConfig() Config {
home, _ := os.UserHomeDir()
return Config{
Port: 8090,
DagsDir: filepath.Join(home, "dagu", "dags"),
DBPath: "dag_engine.db",
}
}
// ParseFlags populates config from CLI flags for the "server" subcommand.
func (c *Config) ParseFlags(fs *flag.FlagSet, args []string) error {
fs.IntVar(&c.Port, "port", c.Port, "HTTP server port")
fs.StringVar(&c.DagsDir, "dags-dir", c.DagsDir, "directory containing DAG YAML files")
fs.StringVar(&c.DBPath, "db", c.DBPath, "path to SQLite database")
fs.BoolVar(&c.AutoScheduler, "scheduler", c.AutoScheduler, "auto-start cron scheduler")
return fs.Parse(args)
}
@@ -1,26 +0,0 @@
name: fn_backup
description: Backup diario de fn_registry (registry.db + operations.db + vaults) via funcion del registry
schedule:
- "0 3 * * *"
tags: [backup, registry, daily]
env:
- BACKUP_ROOT: /home/lucas/backups/fn_registry
steps:
- name: ensure_dirs
command: mkdir -p ${BACKUP_ROOT}
- name: run_backup_all
description: "Snapshot atomico de registry.db + operations.db + vaults con retention 7/4/12"
function: backup_all_bash_pipelines
args: ["${BACKUP_ROOT}"]
continue_on:
exit_code: [4]
depends: [ensure_dirs]
- name: report_status
command: bash -c 'ls -lh ${BACKUP_ROOT}/registry/daily.0 ${BACKUP_ROOT}/operations/*/daily.0 2>/dev/null | tail -20'
depends: [run_backup_all]
@@ -1,51 +0,0 @@
name: revision-viernes-finanzas
description: Revisión semanal de finanzas personales - ingesta, informe y push a Gitea
tags: [finanzas, semanal]
type: graph
schedule: "0 9 * * 5"
env:
- PROJECT_DIR: /home/lucas/analysis/finanzas_personales
- PYTHON: /home/lucas/analysis/finanzas_personales/.venv/bin/python
handler_on:
failure:
command: echo "[$(date)] FALLÓ revision-viernes-finanzas" >> /home/lucas/dagu/logs/failures.log
steps:
- id: ingest
description: Procesar archivos nuevos del inbox (BBVA xlsx + Revolut csv)
working_dir: ${PROJECT_DIR}
command: ./bin/ingest -skip-notebooks
continue_on:
failure: true
- id: informe
description: Generar informe semanal de cumplimiento del presupuesto
command: ${PYTHON} /home/lucas/dagu/scripts/informe_finanzas.py
depends: [ingest]
- id: git_push
description: Commit y push del informe a Gitea
working_dir: ${PROJECT_DIR}
script: |
#!/bin/bash
set -euo pipefail
if git diff --quiet data/04_output/informe_semanal.md 2>/dev/null && \
! git ls-files --others --exclude-standard | grep -q informe_semanal.md; then
echo "Sin cambios en el informe, skip push"
exit 0
fi
git add data/04_output/informe_semanal.md
git add data/03_processed/ 2>/dev/null || true
git commit -m "Informe semanal $(date +%Y-%m-%d)
Co-Authored-By: Dagu Automation <noreply@dagu.dev>"
git push origin master:main
echo "Push completado"
depends: [informe]
-438
View File
@@ -1,438 +0,0 @@
package main
// WebSocket hub para live updates de dag_runs + dag_step_results.
// Patron: sqlite_api/events.go (CallMonitorHub) — issue 0095.
//
// Diseño:
// - Hub global con N subscribers WS.
// - Ticker arranca solo con >=1 subscriber. Cero overhead si nadie mira.
// - Cada tick (500ms): query rowid>watermark + activos (status running/pending)
// + recientes finished (ultimos 5s) -> broadcast upsert.
// - Snapshot inicial: lista de DAGs + ultimos 50 runs + step_results.
// - El cliente trata `runs` y `steps` como upserts por id.
import (
"context"
"database/sql"
"log"
"net/http"
"sync"
"time"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"dag-engine/store"
)
const (
dagWSTickInterval = 500 * time.Millisecond
dagWSTickIntervalIdle = 2 * time.Second
dagWSIdleThreshold = 30 * time.Second
dagWSSnapshotRuns = 50
dagWSBroadcastTimeout = 2 * time.Second
dagWSRecentFinishedS = 5
)
type wsRun struct {
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
}
type wsStep struct {
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt string `json:"started_at,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
type wsWatermark struct {
Runs int64 `json:"runs"`
Steps int64 `json:"steps"`
}
type wsDagMessage struct {
Type string `json:"type"` // snapshot|delta|ping
Watermark wsWatermark `json:"watermark"`
Dags []DagInfo `json:"dags,omitempty"`
Runs []wsRun `json:"runs,omitempty"`
Steps []wsStep `json:"steps,omitempty"`
ServerTime int64 `json:"server_time"`
}
type wsDagClientCmd struct {
Watermark wsWatermark `json:"watermark,omitempty"`
}
type dagSubscriber struct {
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
out chan wsDagMessage
watermark wsWatermark
}
// DagRunHub broadcastea cambios de dag_runs + dag_step_results a clientes WS.
type DagRunHub struct {
db *store.DB
executor *Executor
mu sync.Mutex
subscribers map[*dagSubscriber]struct{}
tickerStop chan struct{}
tickerOn bool
watermark wsWatermark
lastEventAt time.Time
}
func NewDagRunHub(db *store.DB, executor *Executor) *DagRunHub {
return &DagRunHub{
db: db,
executor: executor,
subscribers: make(map[*dagSubscriber]struct{}),
}
}
func (h *DagRunHub) register(s *dagSubscriber) {
h.mu.Lock()
h.subscribers[s] = struct{}{}
shouldStart := !h.tickerOn
if shouldStart {
h.tickerStop = make(chan struct{})
h.tickerOn = true
h.lastEventAt = time.Now()
}
h.mu.Unlock()
if shouldStart {
go h.tickerLoop()
}
}
func (h *DagRunHub) unregister(s *dagSubscriber) {
h.mu.Lock()
if _, ok := h.subscribers[s]; !ok {
h.mu.Unlock()
return
}
delete(h.subscribers, s)
close(s.out)
shouldStop := h.tickerOn && len(h.subscribers) == 0
if shouldStop {
close(h.tickerStop)
h.tickerOn = false
}
h.mu.Unlock()
}
func (h *DagRunHub) tickerLoop() {
interval := dagWSTickInterval
t := time.NewTimer(interval)
defer t.Stop()
for {
select {
case <-h.tickerStop:
return
case <-t.C:
runs, steps, wm, err := h.fetchDelta(h.getWatermark())
if err != nil {
log.Printf("[dagws] fetchDelta: %v", err)
} else if len(runs) > 0 || len(steps) > 0 {
h.setWatermark(wm)
h.recordActivity()
h.broadcast(wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
})
}
if time.Since(h.lastActivityAt()) > dagWSIdleThreshold {
interval = dagWSTickIntervalIdle
} else {
interval = dagWSTickInterval
}
t.Reset(interval)
}
}
}
func (h *DagRunHub) getWatermark() wsWatermark {
h.mu.Lock()
defer h.mu.Unlock()
return h.watermark
}
func (h *DagRunHub) setWatermark(v wsWatermark) {
h.mu.Lock()
if v.Runs > h.watermark.Runs {
h.watermark.Runs = v.Runs
}
if v.Steps > h.watermark.Steps {
h.watermark.Steps = v.Steps
}
h.mu.Unlock()
}
func (h *DagRunHub) recordActivity() {
h.mu.Lock()
h.lastEventAt = time.Now()
h.mu.Unlock()
}
func (h *DagRunHub) lastActivityAt() time.Time {
h.mu.Lock()
defer h.mu.Unlock()
return h.lastEventAt
}
// fetchDelta devuelve runs/steps con (rowid > watermark) OR (status in-flight)
// OR (recently finished). Watermark devuelto = max rowid visto.
func (h *DagRunHub) fetchDelta(since wsWatermark) ([]wsRun, []wsStep, wsWatermark, error) {
conn := h.db.Conn()
if conn == nil {
return nil, nil, since, nil
}
cutoff := time.Now().Add(-time.Duration(dagWSRecentFinishedS) * time.Second).Format(time.RFC3339)
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Runs, cutoff)
if err != nil {
return nil, nil, since, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE rowid > ?
OR status IN ('running','pending')
OR (finished_at IS NOT NULL AND finished_at >= ?)
ORDER BY rowid ASC`, since.Steps, cutoff)
if err != nil {
return runs, nil, since, err
}
out := wsWatermark{Runs: maxRuns, Steps: maxSteps}
if out.Runs < since.Runs {
out.Runs = since.Runs
}
if out.Steps < since.Steps {
out.Steps = since.Steps
}
return runs, steps, out, nil
}
// fetchSnapshot devuelve DAGs + ultimos N runs + sus step_results + watermark.
func (h *DagRunHub) fetchSnapshot() ([]DagInfo, []wsRun, []wsStep, wsWatermark, error) {
dags, err := h.executor.ListDAGs()
if err != nil {
log.Printf("[dagws] list dags: %v", err)
dags = nil
}
conn := h.db.Conn()
if conn == nil {
return dags, nil, nil, wsWatermark{}, nil
}
runs, maxRuns, err := scanRuns(conn, `
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
COALESCE(finished_at,''), error
FROM dag_runs
ORDER BY started_at DESC
LIMIT ?`, dagWSSnapshotRuns)
if err != nil {
return dags, nil, nil, wsWatermark{}, err
}
steps, maxSteps, err := scanSteps(conn, `
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
FROM dag_step_results
WHERE run_id IN (SELECT id FROM dag_runs ORDER BY started_at DESC LIMIT ?)
ORDER BY rowid ASC`, dagWSSnapshotRuns)
if err != nil {
return dags, runs, nil, wsWatermark{Runs: maxRuns}, err
}
return dags, runs, steps, wsWatermark{Runs: maxRuns, Steps: maxSteps}, nil
}
func scanRuns(conn *sql.DB, q string, args ...any) ([]wsRun, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsRun
var max int64
for rows.Next() {
var r wsRun
var rowid int64
if err := rows.Scan(&rowid, &r.ID, &r.DagName, &r.DagPath, &r.Status,
&r.Trigger, &r.StartedAt, &r.FinishedAt, &r.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, r)
}
return out, max, rows.Err()
}
func scanSteps(conn *sql.DB, q string, args ...any) ([]wsStep, int64, error) {
rows, err := conn.Query(q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []wsStep
var max int64
for rows.Next() {
var s wsStep
var rowid int64
if err := rows.Scan(&rowid, &s.ID, &s.RunID, &s.StepName, &s.Status,
&s.ExitCode, &s.Stdout, &s.Stderr, &s.StartedAt, &s.FinishedAt,
&s.DurationMs, &s.Error); err != nil {
return nil, 0, err
}
if rowid > max {
max = rowid
}
out = append(out, s)
}
return out, max, rows.Err()
}
func (h *DagRunHub) broadcast(msg wsDagMessage) {
h.mu.Lock()
subs := make([]*dagSubscriber, 0, len(h.subscribers))
for s := range h.subscribers {
subs = append(subs, s)
}
h.mu.Unlock()
for _, s := range subs {
select {
case s.out <- msg:
default:
log.Printf("[dagws] dropping frame for slow subscriber")
}
}
}
// handleDagRunsWS upgrade WS y gestiona lifecycle.
// Endpoint: GET /api/ws/dagruns
func handleDagRunsWS(hub *DagRunHub) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
if err != nil {
log.Printf("[dagws] accept: %v", err)
return
}
defer conn.Close(websocket.StatusInternalError, "closing")
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
sub := &dagSubscriber{
conn: conn,
ctx: ctx,
cancel: cancel,
out: make(chan wsDagMessage, 64),
}
hub.register(sub)
defer hub.unregister(sub)
dags, runs, steps, wm, err := hub.fetchSnapshot()
if err != nil {
log.Printf("[dagws] snapshot: %v", err)
conn.Close(websocket.StatusInternalError, "snapshot failed")
return
}
hub.setWatermark(wm)
initial := wsDagMessage{
Type: "snapshot",
Watermark: wm,
Dags: dags,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}
if err := wsjson.Write(ctx, conn, initial); err != nil {
return
}
readErr := make(chan error, 1)
go func() {
for {
var cmd wsDagClientCmd
if err := wsjson.Read(ctx, conn, &cmd); err != nil {
readErr <- err
return
}
if cmd.Watermark.Runs > 0 || cmd.Watermark.Steps > 0 {
runs, steps, wm, err := hub.fetchDelta(cmd.Watermark)
if err == nil && (len(runs) > 0 || len(steps) > 0) {
hub.setWatermark(wm)
select {
case sub.out <- wsDagMessage{
Type: "delta",
Watermark: wm,
Runs: runs,
Steps: steps,
ServerTime: time.Now().Unix(),
}:
default:
}
}
}
}
}()
for {
select {
case <-ctx.Done():
return
case err := <-readErr:
if err != nil {
return
}
case msg, ok := <-sub.out:
if !ok {
return
}
wctx, wcancel := context.WithTimeout(ctx, dagWSBroadcastTimeout)
err := wsjson.Write(wctx, conn, msg)
wcancel()
if err != nil {
return
}
}
}
}
}
-515
View File
@@ -1,515 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"fn-registry/functions/core"
"fn-registry/functions/infra"
"dag-engine/store"
)
// Executor orchestrates DAG parsing, validation, and execution.
type Executor struct {
store *store.DB
dagsDir string
}
// NewExecutor creates a new executor.
func NewExecutor(s *store.DB, dagsDir string) *Executor {
return &Executor{store: s, dagsDir: dagsDir}
}
// ExecuteDAG runs a DAG from a YAML file path and returns the run ID.
// It runs asynchronously: steps execute in topological order with parallel levels.
func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger string) (string, error) {
data, err := os.ReadFile(dagPath)
if err != nil {
return "", fmt.Errorf("read dag: %w", err)
}
dag, err := core.DagParse(data)
if err != nil {
return "", fmt.Errorf("parse dag: %w", err)
}
dag.FilePath = dagPath
// Resolve env variables.
dag = core.DagResolveEnv(dag, os.Environ())
// Validate.
result := core.DagValidate(dag)
if !result.Valid {
return "", fmt.Errorf("validate dag: %s", strings.Join(result.Errors, "; "))
}
// Create run record.
runID := generateID()
now := time.Now()
run := &store.DagRun{
ID: runID,
DagName: dag.Name,
DagPath: dagPath,
Status: "running",
Trigger: trigger,
StartedAt: now,
}
if err := e.store.CreateRun(run); err != nil {
return "", fmt.Errorf("create run: %w", err)
}
// Topological sort.
levels, err := core.DagTopoSort(dag.Steps)
if err != nil {
e.failRun(runID, err)
return runID, err
}
// Setup DAGU_ENV temp file for inter-step communication.
daguEnvFile, err := os.CreateTemp("", "dagu_env_*")
if err != nil {
e.failRun(runID, err)
return runID, err
}
daguEnvPath := daguEnvFile.Name()
daguEnvFile.Close()
defer os.Remove(daguEnvPath)
// Track step outputs for ${step_id.stdout} references.
stepOutputs := make(map[string]string)
// Execute levels.
runFailed := false
var runErr error
for _, level := range levels {
if runFailed {
// Skip remaining levels, mark steps as skipped.
for _, step := range level {
e.recordStepSkipped(runID, step)
}
continue
}
var wg sync.WaitGroup
var mu sync.Mutex
levelFailed := false
for _, step := range level {
step := step
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
if levelFailed {
mu.Unlock()
e.recordStepSkipped(runID, step)
return
}
mu.Unlock()
err := e.executeStep(ctx, runID, dag, step, daguEnvPath, stepOutputs, &mu)
if err != nil && !step.ContinueOn.Failure {
mu.Lock()
levelFailed = true
runFailed = true
runErr = fmt.Errorf("step %q failed: %w", stepName(step), err)
mu.Unlock()
}
}()
}
wg.Wait()
}
// Run handlers.
if runFailed {
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Failure, daguEnvPath, stepOutputs)
} else {
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Success, daguEnvPath, stepOutputs)
}
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Exit, daguEnvPath, stepOutputs)
// Finalize run.
fin := time.Now()
status := "success"
errMsg := ""
if runFailed {
status = "failed"
if runErr != nil {
errMsg = runErr.Error()
}
}
e.store.UpdateRunStatus(runID, status, &fin, errMsg)
return runID, runErr
}
// executeStep runs a single step, recording results in the store.
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
stepID := generateID()
now := time.Now()
// Resolve command source: function (registry) takes precedence over command/script.
var command string
var stepFunctionID string
var fnRegistryRoot string
if step.Function != "" {
stepFunctionID = step.Function
fnRegistryRoot = os.Getenv("FN_REGISTRY_ROOT")
if fnRegistryRoot == "" {
fnRegistryRoot = "/home/lucas/fn_registry"
}
fnBin := os.Getenv("FN_BIN")
if fnBin == "" {
fnBin = fnRegistryRoot + "/fn"
}
parts := []string{fnBin, "run", step.Function}
parts = append(parts, step.Args...)
command = strings.Join(parts, " ")
} else if step.Command != "" {
command = step.Command
} else if step.Script != "" {
command = step.Script
}
e.store.InsertStepResult(&store.DagStepResult{
ID: stepID,
RunID: runID,
StepName: stepName(step),
FunctionID: stepFunctionID,
Status: "running",
StartedAt: &now,
})
// Build environment.
env := buildStepEnv(dag, step, daguEnvPath, outputs)
// For function: steps, force FN_REGISTRY_ROOT into env so `fn run`
// resolves the canonical registry.db (not whatever lives at the spawn cwd).
// Prevents the apps/dag_engine/registry.db stale-shadow bug (2026-05-16).
if stepFunctionID != "" {
env = append(env, "FN_REGISTRY_ROOT="+fnRegistryRoot)
}
if command == "" {
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
return nil
}
// Resolve step-level ${VAR} references and ${step_id.stdout} patterns.
mu.Lock()
command = resolveStepRefs(command, outputs)
mu.Unlock()
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
// so `fn` resolves registry.db correctly via go.mod walk-up.
dir := step.Dir
if dir == "" {
dir = dag.WorkingDir
}
if dir == "" && stepFunctionID != "" {
dir = fnRegistryRoot
}
shell := step.Shell
if shell == "" {
shell = dag.Shell
}
// Spawn process.
handle, err := infra.ProcessSpawn(command, dir, env, shell)
if err != nil {
fin := time.Now()
e.store.UpdateStepResult(stepID, "failed", -1, "", "", &fin, time.Since(now).Milliseconds(), err.Error())
return err
}
// Wait for process.
result, err := infra.ProcessWait(handle, step.TimeoutSec)
fin := time.Now()
duration := time.Since(now).Milliseconds()
if err != nil && result.ExitCode == 0 {
result.ExitCode = -1
}
status := "success"
errMsg := ""
if result.ExitCode != 0 || err != nil {
status = "failed"
if err != nil {
errMsg = err.Error()
}
}
e.store.UpdateStepResult(stepID, status, result.ExitCode, result.Stdout, result.Stderr, &fin, duration, errMsg)
// Store output for ${step_id.stdout} references.
if step.ID != "" || step.Output != "" {
mu.Lock()
key := step.ID
if key == "" {
key = step.Output
}
outputs[key] = strings.TrimSpace(result.Stdout)
mu.Unlock()
}
// Read DAGU_ENV for inter-step env propagation.
readDaguEnv(daguEnvPath, outputs)
if status == "failed" {
return fmt.Errorf("exit code %d", result.ExitCode)
}
return nil
}
func (e *Executor) runHandlers(ctx context.Context, runID string, dag core.DagDefinition, handlers []core.DagStep, daguEnvPath string, outputs map[string]string) {
var mu sync.Mutex
for _, step := range handlers {
e.executeStep(ctx, runID, dag, step, daguEnvPath, outputs, &mu)
}
}
func (e *Executor) failRun(runID string, err error) {
fin := time.Now()
e.store.UpdateRunStatus(runID, "failed", &fin, err.Error())
}
func (e *Executor) recordStepSkipped(runID string, step core.DagStep) {
now := time.Now()
e.store.InsertStepResult(&store.DagStepResult{
ID: generateID(),
RunID: runID,
StepName: stepName(step),
Status: "skipped",
StartedAt: &now,
})
}
// --- helpers ---
func stepName(s core.DagStep) string {
if s.Name != "" {
return s.Name
}
return s.ID
}
func buildStepEnv(dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string) []string {
env := os.Environ()
// Add DAG-level env.
for k, v := range dag.Env {
env = append(env, k+"="+v)
}
// Add step-level env.
for k, v := range step.Env {
env = append(env, k+"="+v)
}
// Add DAGU_ENV path.
env = append(env, "DAGU_ENV="+daguEnvPath)
return env
}
func resolveStepRefs(command string, outputs map[string]string) string {
for k, v := range outputs {
command = strings.ReplaceAll(command, "${"+k+".stdout}", v)
command = strings.ReplaceAll(command, "$"+k+".stdout", v)
}
return command
}
func readDaguEnv(path string, outputs map[string]string) {
data, err := os.ReadFile(path)
if err != nil || len(data) == 0 {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
outputs[parts[0]] = parts[1]
}
}
}
// generateID creates a simple time-based unique ID.
func generateID() string {
return fmt.Sprintf("%d-%04x", time.Now().UnixNano(), time.Now().Nanosecond()%0xFFFF)
}
// --- DAG listing helpers ---
// DagInfo summarizes a DAG file for listing.
type DagInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
LastRuns []store.DagRun `json:"last_runs,omitempty"` // 5 mas recientes (mas reciente primero)
}
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
func (e *Executor) ListDAGs() ([]DagInfo, error) {
entries, err := os.ReadDir(e.dagsDir)
if err != nil {
return nil, fmt.Errorf("read dags dir: %w", err)
}
var dags []DagInfo
for _, entry := range entries {
if entry.IsDir() {
continue
}
ext := filepath.Ext(entry.Name())
if ext != ".yaml" && ext != ".yml" {
continue
}
path := filepath.Join(e.dagsDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
continue
}
dag, err := core.DagParse(data)
if err != nil {
dags = append(dags, DagInfo{
Name: strings.TrimSuffix(entry.Name(), ext),
FilePath: path,
Valid: false,
})
continue
}
info := DagInfo{
Name: dag.Name,
Description: dag.Description,
Schedule: dag.Schedule,
Tags: dag.Tags,
Type: dag.Type,
FilePath: path,
Valid: true,
}
// 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)
}
return dags, nil
}
// GetDAG returns detailed info for a specific DAG by name.
func (e *Executor) GetDAG(name string) (*DagInfo, *core.DagDefinition, *core.DagValidationResult, error) {
// Find the YAML file.
entries, err := os.ReadDir(e.dagsDir)
if err != nil {
return nil, nil, nil, err
}
for _, entry := range entries {
ext := filepath.Ext(entry.Name())
base := strings.TrimSuffix(entry.Name(), ext)
if (ext != ".yaml" && ext != ".yml") || base != name {
continue
}
path := filepath.Join(e.dagsDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, nil, err
}
dag, err := core.DagParse(data)
if err != nil {
return nil, nil, nil, fmt.Errorf("parse: %w", err)
}
dag.FilePath = path
validationResult := core.DagValidate(dag)
info := &DagInfo{
Name: dag.Name,
Description: dag.Description,
Schedule: dag.Schedule,
Tags: dag.Tags,
Type: dag.Type,
FilePath: path,
Valid: validationResult.Valid,
}
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
if len(runs) > 0 {
info.LastRun = &runs[0]
}
return info, &dag, &validationResult, nil
}
return nil, nil, nil, fmt.Errorf("dag %q not found in %s", name, e.dagsDir)
}
// ValidateDAG parses and validates a DAG file, printing results.
func ValidateDAG(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
dag, err := core.DagParse(data)
if err != nil {
return fmt.Errorf("parse error: %w", err)
}
result := core.DagValidate(dag)
log.Printf("DAG: %s", dag.Name)
log.Printf("Steps: %d", len(dag.Steps))
log.Printf("Schedule: %v", dag.Schedule)
if result.Valid {
log.Printf("Validation: PASS")
log.Printf("Topological levels: %d", len(result.Levels))
for i, level := range result.Levels {
log.Printf(" Level %d: %v", i, level)
}
} else {
log.Printf("Validation: FAIL")
for _, e := range result.Errors {
log.Printf(" ERROR: %s", e)
}
}
for _, w := range result.Warnings {
log.Printf(" WARNING: %s", w)
}
if !result.Valid {
return fmt.Errorf("validation failed")
}
return nil
}
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAG Engine</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-28
View File
@@ -1,28 +0,0 @@
{
"name": "dag-engine-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@mantine/core": "^9.0.2",
"@mantine/hooks": "^9.0.2",
"@tabler/icons-react": "^3.31.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1"
},
"devDependencies": {
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
"postcss": "^8.5.4",
"postcss-preset-mantine": "^1.17.0",
"typescript": "~5.8.3",
"vite": "^6.3.5"
}
}
@@ -1,5 +0,0 @@
module.exports = {
plugins: {
"postcss-preset-mantine": {},
},
};
-32
View File
@@ -1,32 +0,0 @@
import { Routes, Route } from "react-router-dom";
import { AppShell, Container, Title, Group, Text } from "@mantine/core";
import { IconTopologyRing } from "@tabler/icons-react";
import { DagList } from "./pages/DagList";
import { DagDetail } from "./pages/DagDetail";
import { RunDetail } from "./pages/RunDetail";
export function App() {
return (
<AppShell header={{ height: 50 }} padding="md">
<AppShell.Header>
<Group h="100%" px="md">
<IconTopologyRing size={24} />
<Title order={4}>DAG Engine</Title>
<Text size="xs" c="dimmed">
fn_registry workflow executor
</Text>
</Group>
</AppShell.Header>
<AppShell.Main>
<Container size="lg">
<Routes>
<Route path="/" element={<DagList />} />
<Route path="/dags/:name" element={<DagDetail />} />
<Route path="/runs/:id" element={<RunDetail />} />
</Routes>
</Container>
</AppShell.Main>
</AppShell>
);
}
-63
View File
@@ -1,63 +0,0 @@
import type {
DagSummary,
DagDetail,
DagRun,
RunDetail,
SchedulerStatus,
} from "./types";
const BASE = "/api";
async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, init);
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || res.statusText);
}
return res.json();
}
export function listDags(): Promise<DagSummary[]> {
return fetchJSON("/dags");
}
export function getDag(name: string): Promise<DagDetail> {
return fetchJSON(`/dags/${encodeURIComponent(name)}`);
}
export function triggerDag(
name: string
): Promise<{ status: string; dag: string; message: string }> {
return fetchJSON(`/dags/${encodeURIComponent(name)}/run`, {
method: "POST",
});
}
export function listRuns(params?: {
dag?: string;
limit?: number;
offset?: number;
}): Promise<{ runs: DagRun[]; total: number }> {
const search = new URLSearchParams();
if (params?.dag) search.set("dag", params.dag);
if (params?.limit) search.set("limit", String(params.limit));
if (params?.offset) search.set("offset", String(params.offset));
const qs = search.toString();
return fetchJSON(`/runs${qs ? "?" + qs : ""}`);
}
export function getRun(id: string): Promise<RunDetail> {
return fetchJSON(`/runs/${encodeURIComponent(id)}`);
}
export function startScheduler(): Promise<void> {
return fetchJSON("/scheduler/start", { method: "POST" });
}
export function stopScheduler(): Promise<void> {
return fetchJSON("/scheduler/stop", { method: "POST" });
}
export function getSchedulerStatus(): Promise<SchedulerStatus> {
return fetchJSON("/scheduler/status");
}
@@ -1,18 +0,0 @@
import { Badge } from "@mantine/core";
const colorMap: Record<string, string> = {
success: "green",
failed: "red",
running: "blue",
pending: "gray",
cancelled: "yellow",
skipped: "dimmed",
};
export function StatusBadge({ status }: { status: string }) {
return (
<Badge color={colorMap[status] || "gray"} variant="light" size="sm">
{status}
</Badge>
);
}
@@ -1,85 +0,0 @@
import { Timeline, Text, Code, Collapse, Box, Group } from "@mantine/core";
import {
IconCircleCheck,
IconCircleX,
IconLoader,
IconCircleMinus,
IconClock,
} from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";
import type { DagStepResult } from "../types";
const iconMap: Record<string, React.ReactNode> = {
success: <IconCircleCheck size={16} color="var(--mantine-color-green-6)" />,
failed: <IconCircleX size={16} color="var(--mantine-color-red-6)" />,
running: <IconLoader size={16} color="var(--mantine-color-blue-6)" />,
skipped: <IconCircleMinus size={16} color="var(--mantine-color-dimmed)" />,
pending: <IconClock size={16} color="var(--mantine-color-gray-6)" />,
};
function StepItem({ step }: { step: DagStepResult }) {
const [opened, { toggle }] = useDisclosure(step.Status === "failed");
const hasOutput = step.Stdout || step.Stderr;
return (
<Timeline.Item
bullet={iconMap[step.Status] || iconMap.pending}
title={
<Group gap="xs">
<Text
size="sm"
fw={500}
onClick={hasOutput ? toggle : undefined}
style={hasOutput ? { cursor: "pointer" } : undefined}
>
{step.StepName}
</Text>
<Text size="xs" c="dimmed">
{step.DurationMs}ms
</Text>
{step.ExitCode !== 0 && step.ExitCode !== -1 && (
<Text size="xs" c="red">
exit {step.ExitCode}
</Text>
)}
</Group>
}
>
{hasOutput && (
<Collapse in={opened}>
<Box mt="xs">
{step.Stdout && (
<Code block mb="xs" style={{ maxHeight: 200, overflow: "auto" }}>
{step.Stdout}
</Code>
)}
{step.Stderr && (
<Code
block
color="red"
style={{ maxHeight: 200, overflow: "auto" }}
>
{step.Stderr}
</Code>
)}
</Box>
</Collapse>
)}
</Timeline.Item>
);
}
export function StepTimeline({ steps }: { steps: DagStepResult[] }) {
const activeIndex = steps.findIndex((s) => s.Status === "running");
return (
<Timeline
active={activeIndex >= 0 ? activeIndex : steps.length - 1}
bulletSize={24}
>
{steps.map((step) => (
<StepItem key={step.ID} step={step} />
))}
</Timeline>
);
}
-18
View File
@@ -1,18 +0,0 @@
import "@mantine/core/styles.css";
import { MantineProvider, createTheme } from "@mantine/core";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { App } from "./App";
const theme = createTheme({
primaryColor: "blue",
fontFamily: "system-ui, -apple-system, sans-serif",
});
createRoot(document.getElementById("root")!).render(
<MantineProvider theme={theme} defaultColorScheme="dark">
<BrowserRouter>
<App />
</BrowserRouter>
</MantineProvider>
);
@@ -1,204 +0,0 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
Title,
Text,
Group,
Button,
Badge,
Stack,
Paper,
Table,
Alert,
Loader,
Code,
} from "@mantine/core";
import { IconPlayerPlay, IconArrowLeft } from "@tabler/icons-react";
import { getDag, triggerDag } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import type { DagDetail as DagDetailType } from "../types";
export function DagDetail() {
const { name } = useParams<{ name: string }>();
const navigate = useNavigate();
const [data, setData] = useState<DagDetailType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [triggering, setTriggering] = useState(false);
const load = async () => {
if (!name) return;
setLoading(true);
try {
setData(await getDag(name));
setError(null);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, [name]);
const handleRun = async () => {
if (!name) return;
setTriggering(true);
try {
await triggerDag(name);
setTimeout(load, 1000);
} catch (e) {
setError((e as Error).message);
} finally {
setTriggering(false);
}
};
if (loading) return <Loader />;
if (error) return <Alert color="red">{error}</Alert>;
if (!data) return <Text>Not found</Text>;
const { dag, validation, runs } = data;
return (
<Stack gap="md">
<Group>
<Button
variant="subtle"
size="xs"
leftSection={<IconArrowLeft size={14} />}
onClick={() => navigate("/")}
>
Back
</Button>
</Group>
<Group justify="space-between">
<div>
<Title order={2}>{dag.Name}</Title>
{dag.Description && (
<Text size="sm" c="dimmed">
{dag.Description}
</Text>
)}
</div>
<Button
leftSection={<IconPlayerPlay size={16} />}
onClick={handleRun}
loading={triggering}
>
Run Now
</Button>
</Group>
<Group gap="xs">
{dag.Schedule?.map((s: string) => (
<Badge key={s} variant="light" ff="monospace">
{s}
</Badge>
))}
<Badge variant="light">{dag.Type || "chain"}</Badge>
{dag.Tags?.map((t: string) => (
<Badge key={t} variant="dot">
{t}
</Badge>
))}
</Group>
{!validation.Valid && (
<Alert color="red" title="Validation errors">
{validation.Errors.map((e: string, i: number) => (
<Text key={i} size="sm">
{e}
</Text>
))}
</Alert>
)}
<Paper p="md" withBorder>
<Title order={4} mb="sm">
Steps ({dag.Steps?.length || 0})
</Title>
{validation.Levels?.map((level: string[], i: number) => (
<Group key={i} gap="xs" mb="xs">
<Text size="xs" c="dimmed" w={60}>
Level {i}:
</Text>
{level.map((name: string) => {
const step = dag.Steps?.find(
(s) => s.Name === name || s.ID === name
);
return (
<Badge key={name} variant="outline" size="sm">
{name}
{step?.Depends?.length
? ` (after ${step.Depends.join(",")})`
: ""}
</Badge>
);
})}
</Group>
))}
{dag.Env && Object.keys(dag.Env).length > 0 && (
<>
<Title order={5} mt="md" mb="xs">
Environment
</Title>
<Code block>
{Object.entries(dag.Env)
.map(([k, v]) => `${k}=${v}`)
.join("\n")}
</Code>
</>
)}
</Paper>
<Paper p="md" withBorder>
<Title order={4} mb="sm">
Run History
</Title>
{runs?.length ? (
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Status</Table.Th>
<Table.Th>Trigger</Table.Th>
<Table.Th>Started</Table.Th>
<Table.Th>Duration</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{runs.map((r) => (
<Table.Tr
key={r.ID}
style={{ cursor: "pointer" }}
onClick={() => navigate(`/runs/${r.ID}`)}
>
<Table.Td>
<StatusBadge status={r.Status} />
</Table.Td>
<Table.Td>{r.Trigger}</Table.Td>
<Table.Td>
{new Date(r.StartedAt).toLocaleString()}
</Table.Td>
<Table.Td>
{r.FinishedAt
? `${Math.round((new Date(r.FinishedAt).getTime() - new Date(r.StartedAt).getTime()) / 1000)}s`
: "running..."}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
) : (
<Text size="sm" c="dimmed">
No runs yet
</Text>
)}
</Paper>
</Stack>
);
}
@@ -1,164 +0,0 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Table,
Title,
Group,
Button,
Badge,
Text,
Loader,
Stack,
Alert,
} from "@mantine/core";
import {
IconPlayerPlay,
IconPlayerStop,
IconRefresh,
} from "@tabler/icons-react";
import { listDags, getSchedulerStatus, startScheduler, stopScheduler } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import type { DagSummary, SchedulerStatus } from "../types";
export function DagList() {
const [dags, setDags] = useState<DagSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [scheduler, setScheduler] = useState<SchedulerStatus | null>(null);
const navigate = useNavigate();
const load = async () => {
setLoading(true);
setError(null);
try {
const [d, s] = await Promise.all([listDags(), getSchedulerStatus()]);
setDags(d || []);
setScheduler(s);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
const interval = setInterval(load, 10000);
return () => clearInterval(interval);
}, []);
const toggleScheduler = async () => {
if (scheduler?.running) {
await stopScheduler();
} else {
await startScheduler();
}
const s = await getSchedulerStatus();
setScheduler(s);
};
return (
<Stack gap="md">
<Group justify="space-between">
<Title order={2}>DAGs</Title>
<Group gap="xs">
<Button
size="xs"
variant="light"
leftSection={<IconRefresh size={14} />}
onClick={load}
>
Refresh
</Button>
<Button
size="xs"
variant={scheduler?.running ? "filled" : "light"}
color={scheduler?.running ? "green" : "gray"}
leftSection={
scheduler?.running ? (
<IconPlayerStop size={14} />
) : (
<IconPlayerPlay size={14} />
)
}
onClick={toggleScheduler}
>
Scheduler {scheduler?.running ? "ON" : "OFF"}
</Button>
</Group>
</Group>
{error && <Alert color="red">{error}</Alert>}
{loading && !dags.length ? (
<Loader />
) : (
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Schedule</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Tags</Table.Th>
<Table.Th>Last Status</Table.Th>
<Table.Th>Last Run</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{dags.map((d) => (
<Table.Tr
key={d.file_path}
style={{ cursor: "pointer" }}
onClick={() => navigate(`/dags/${d.name}`)}
>
<Table.Td>
<Text fw={500}>{d.name}</Text>
{d.description && (
<Text size="xs" c="dimmed" lineClamp={1}>
{d.description}
</Text>
)}
</Table.Td>
<Table.Td>
<Text size="xs" ff="monospace">
{d.schedule?.join(", ") || "-"}
</Text>
</Table.Td>
<Table.Td>
<Badge variant="light" size="xs">
{d.type || "chain"}
</Badge>
</Table.Td>
<Table.Td>
<Group gap={4}>
{d.tags?.map((t) => (
<Badge key={t} variant="dot" size="xs">
{t}
</Badge>
))}
</Group>
</Table.Td>
<Table.Td>
{d.last_run ? (
<StatusBadge status={d.last_run.Status} />
) : (
<Text size="xs" c="dimmed">
-
</Text>
)}
</Table.Td>
<Table.Td>
<Text size="xs">
{d.last_run
? new Date(d.last_run.StartedAt).toLocaleString()
: "-"}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
</Stack>
);
}
@@ -1,191 +0,0 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
Title,
Text,
Group,
Button,
Stack,
Paper,
Alert,
Loader,
CopyButton,
Tooltip,
ActionIcon,
Code,
} from "@mantine/core";
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, 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 }>();
const navigate = useNavigate();
const [data, setData] = useState<RunDetailType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = async () => {
if (!id) return;
try {
setData(await getRun(id));
setError(null);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
// Auto-refresh while running.
const interval = setInterval(() => {
if (data?.run.Status === "running") {
load();
}
}, 2000);
return () => clearInterval(interval);
}, [id, data?.run.Status]);
if (loading) return <Loader />;
if (error) return <Alert color="red">{error}</Alert>;
if (!data) return <Text>Not found</Text>;
const { run, steps } = data;
const duration = run.FinishedAt
? `${Math.round((new Date(run.FinishedAt).getTime() - new Date(run.StartedAt).getTime()) / 1000)}s`
: "running...";
return (
<Stack gap="md">
<Group>
<Button
variant="subtle"
size="xs"
leftSection={<IconArrowLeft size={14} />}
onClick={() => navigate(`/dags/${run.DagName}`)}
>
Back to {run.DagName}
</Button>
</Group>
<Group justify="space-between">
<div>
<Title order={2}>Run {run.ID.substring(0, 16)}...</Title>
<Text size="sm" c="dimmed">
{run.DagName} &middot; {run.Trigger} &middot;{" "}
{new Date(run.StartedAt).toLocaleString()}
</Text>
</div>
<Group gap="xs">
<StatusBadge status={run.Status} />
<Text size="sm">{duration}</Text>
</Group>
</Group>
{run.Error && (
<Alert color="red" title="Error">
{run.Error}
</Alert>
)}
<Paper p="md" withBorder>
<Title order={4} mb="md">
Steps ({steps?.length || 0})
</Title>
{steps?.length ? (
<StepTimeline steps={steps} />
) : (
<Text size="sm" c="dimmed">
No steps recorded
</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>
);
}
-66
View File
@@ -1,66 +0,0 @@
export interface DagSummary {
name: string;
description?: string;
schedule?: string[];
tags?: string[];
type?: string;
file_path: string;
valid: boolean;
last_run?: DagRun;
}
export interface DagRun {
ID: string;
DagName: string;
DagPath: string;
Status: string;
Trigger: string;
StartedAt: string;
FinishedAt?: string;
Error: string;
}
export interface DagStepResult {
ID: string;
RunID: string;
StepName: string;
Status: string;
ExitCode: number;
Stdout: string;
Stderr: string;
StartedAt?: string;
FinishedAt?: string;
DurationMs: number;
Error: string;
}
export interface DagDetail {
info: DagSummary;
dag: {
Name: string;
Description: string;
Type: string;
Schedule: string[];
Steps: { Name: string; ID: string; Command: string; Script: string; Depends: string[] }[];
Env: Record<string, string>;
Tags: string[];
HandlerOn: { Failure: unknown[]; Success: unknown[] };
};
validation: {
Valid: boolean;
Errors: string[];
Warnings: string[];
Levels: string[][];
};
runs: DagRun[];
}
export interface RunDetail {
run: DagRun;
steps: DagStepResult[];
}
export interface SchedulerStatus {
running: boolean;
dags: { name: string; path: string; schedule: string; next_run: string }[];
}
-21
View File
@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
-15
View File
@@ -1,15 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5175,
proxy: {
"/api": "http://localhost:8090",
},
},
build: {
outDir: "dist",
},
});
-51
View File
@@ -1,51 +0,0 @@
module dag-engine
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 (
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apache/arrow-go/v18 v18.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/flatbuffers v25.1.24+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
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/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace fn-registry => /home/lucas/fn_registry
-176
View File
@@ -1,176 +0,0 @@
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y=
github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0=
github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o=
github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0=
github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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=
-76
View File
@@ -1,76 +0,0 @@
package main
import (
"context"
"encoding/json"
"net/http"
)
func handleListDags(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dags, err := executor.ListDAGs()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, dags)
}
}
func handleGetDag(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
info, dag, validation, err := executor.GetDAG(name)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
// Get recent runs.
runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0)
resp := map[string]interface{}{
"info": info,
"dag": dag,
"validation": validation,
"recent_runs": runs,
}
writeJSON(w, http.StatusOK, resp)
}
}
func handleRunDag(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
info, _, _, err := executor.GetDAG(name)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
// Execute asynchronously.
go func() {
ctx := context.Background()
executor.ExecuteDAG(ctx, info.FilePath, "api")
}()
// Return run acknowledgment.
writeJSON(w, http.StatusAccepted, map[string]string{
"status": "accepted",
"dag": name,
"message": "DAG execution started",
})
}
}
// --- JSON helpers ---
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
-59
View File
@@ -1,59 +0,0 @@
package main
import (
"net/http"
"strconv"
)
func handleListRuns(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dagName := r.URL.Query().Get("dag")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit <= 0 || limit > 100 {
limit = 20
}
if offset < 0 {
offset = 0
}
runs, total, err := executor.store.ListRuns(dagName, limit, offset)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"runs": runs,
"total": total,
"limit": limit,
"offset": offset,
})
}
}
func handleGetRun(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
run, err := executor.store.GetRun(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if run == nil {
writeError(w, http.StatusNotFound, "run not found")
return
}
steps, err := executor.store.ListStepResults(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"run": run,
"steps": steps,
})
}
}
-27
View File
@@ -1,27 +0,0 @@
package main
import "net/http"
func handleSchedulerStart(scheduler *Scheduler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := scheduler.Start(); err != nil {
writeError(w, http.StatusConflict, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "started"})
}
}
func handleSchedulerStop(scheduler *Scheduler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scheduler.Stop()
writeJSON(w, http.StatusOK, map[string]string{"status": "stopped"})
}
}
func handleSchedulerStatus(scheduler *Scheduler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
status := scheduler.Status()
writeJSON(w, http.StatusOK, status)
}
}
-337
View File
@@ -1,337 +0,0 @@
package main
import (
"context"
"embed"
"flag"
"fmt"
iofs "io/fs"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"text/tabwriter"
"time"
"fn-registry/functions/core"
"dag-engine/store"
)
//go:embed all:frontend/dist
var frontendDist embed.FS
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
cmd := os.Args[1]
args := os.Args[2:]
switch cmd {
case "run":
cmdRun(args)
case "list":
cmdList(args)
case "status":
cmdStatus(args)
case "validate":
cmdValidate(args)
case "server":
cmdServer(args)
case "help", "-h", "--help":
printUsage()
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println(`dag-engine — DAG workflow executor
Usage:
dag-engine <command> [options]
Commands:
run <path.yaml> Execute a DAG and show results
list [dir] List DAGs with schedule and last status
status [dag_name] Show execution history
validate <path.yaml> Parse and validate without executing
server Start HTTP server with web frontend
Server options:
--port <port> HTTP port (default: 8090)
--dags-dir <dir> DAGs directory (default: ~/dagu/dags)
--db <path> SQLite database path (default: dag_engine.db)
--scheduler Auto-start cron scheduler`)
}
// --- CLI Commands ---
func cmdRun(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: dag-engine run <path.yaml>")
os.Exit(1)
}
dagPath := args[0]
cfg := DefaultConfig()
// Parse optional flags after the path.
fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
fs.Parse(args[1:])
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
executor := NewExecutor(db, filepath.Dir(dagPath))
fmt.Printf("Executing %s...\n", dagPath)
ctx := context.Background()
runID, err := executor.ExecuteDAG(ctx, dagPath, "manual")
// Print results.
if runID != "" {
run, _ := db.GetRun(runID)
steps, _ := db.ListStepResults(runID)
if run != nil {
fmt.Println()
for _, s := range steps {
icon := " "
switch s.Status {
case "success":
icon = "OK"
case "failed":
icon = "!!"
case "skipped":
icon = "--"
case "running":
icon = ".."
}
fmt.Printf("[%s] %s (%dms)\n", icon, s.StepName, s.DurationMs)
if s.Status == "failed" && s.Stderr != "" {
for _, line := range strings.Split(strings.TrimSpace(s.Stderr), "\n") {
fmt.Printf(" %s\n", line)
}
}
}
fmt.Println()
dur := ""
if run.FinishedAt != nil {
dur = fmt.Sprintf(" (%s)", run.FinishedAt.Sub(run.StartedAt).Round(time.Millisecond))
}
fmt.Printf("Run %s: %s%s\n", runID, strings.ToUpper(run.Status), dur)
}
}
if err != nil {
os.Exit(1)
}
}
func cmdList(args []string) {
cfg := DefaultConfig()
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
cfg.DagsDir = args[0]
args = args[1:]
}
fs := flag.NewFlagSet("list", flag.ExitOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
fs.Parse(args)
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
executor := NewExecutor(db, cfg.DagsDir)
dags, err := executor.ListDAGs()
if err != nil {
log.Fatalf("list dags: %v", err)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tSCHEDULE\tTYPE\tTAGS\tLAST STATUS\tLAST RUN")
for _, d := range dags {
sched := strings.Join(d.Schedule, ", ")
tags := strings.Join(d.Tags, ", ")
lastStatus := "-"
lastRun := "-"
if d.LastRun != nil {
lastStatus = d.LastRun.Status
lastRun = d.LastRun.StartedAt.Format("2006-01-02 15:04")
}
typ := d.Type
if typ == "" {
typ = "chain"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", d.Name, sched, typ, tags, lastStatus, lastRun)
}
w.Flush()
}
func cmdStatus(args []string) {
cfg := DefaultConfig()
fs := flag.NewFlagSet("status", flag.ExitOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
limit := fs.Int("limit", 10, "number of runs to show")
fs.Parse(args)
dagName := ""
if fs.NArg() > 0 {
dagName = fs.Arg(0)
}
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
runs, total, err := db.ListRuns(dagName, *limit, 0)
if err != nil {
log.Fatalf("list runs: %v", err)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Showing %d of %d runs", len(runs), total)
if dagName != "" {
fmt.Fprintf(w, " for %s", dagName)
}
fmt.Fprintln(w)
fmt.Fprintln(w, "RUN_ID\tDAG\tSTATUS\tTRIGGER\tSTARTED\tDURATION")
for _, r := range runs {
dur := "-"
if r.FinishedAt != nil {
dur = r.FinishedAt.Sub(r.StartedAt).Round(time.Millisecond).String()
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
r.ID, r.DagName, r.Status, r.Trigger,
r.StartedAt.Format("2006-01-02 15:04:05"), dur)
}
w.Flush()
}
func cmdValidate(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: dag-engine validate <path.yaml>")
os.Exit(1)
}
data, err := os.ReadFile(args[0])
if err != nil {
log.Fatalf("read: %v", err)
}
dag, err := core.DagParse(data)
if err != nil {
log.Fatalf("parse error: %v", err)
}
result := core.DagValidate(dag)
fmt.Printf("DAG: %s\n", dag.Name)
fmt.Printf("Steps: %d\n", len(dag.Steps))
fmt.Printf("Schedule: %v\n", dag.Schedule)
fmt.Printf("Type: %s\n", dag.Type)
if result.Valid {
fmt.Println("Validation: PASS")
for i, level := range result.Levels {
fmt.Printf(" Level %d: %v\n", i, level)
}
} else {
fmt.Println("Validation: FAIL")
for _, e := range result.Errors {
fmt.Printf(" ERROR: %s\n", e)
}
}
for _, w := range result.Warnings {
fmt.Printf(" WARNING: %s\n", w)
}
if !result.Valid {
os.Exit(1)
}
}
// --- Server Command ---
func cmdServer(args []string) {
cfg := DefaultConfig()
fs := flag.NewFlagSet("server", flag.ExitOnError)
cfg.ParseFlags(fs, args)
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
executor := NewExecutor(db, cfg.DagsDir)
scheduler := NewScheduler(executor, cfg.DagsDir)
dagRunHub := NewDagRunHub(db, executor)
// Prepare frontend FS.
var feFS iofs.FS
distFS, err := iofs.Sub(frontendDist, "frontend/dist")
if err == nil {
// Check if dist has content (built frontend exists).
entries, _ := iofs.ReadDir(distFS, ".")
if len(entries) > 0 {
feFS = distFS
log.Printf("serving frontend from embedded dist/")
}
}
if feFS == nil {
log.Printf("no frontend build found, API-only mode")
}
mux := http.NewServeMux()
RegisterAPI(mux, executor, scheduler, dagRunHub, feFS)
handler := corsMiddleware(loggingMiddleware(mux))
if cfg.AutoScheduler {
if err := scheduler.Start(); err != nil {
log.Printf("scheduler start: %v", err)
}
}
addr := fmt.Sprintf(":%d", cfg.Port)
log.Printf("dag-engine server starting on http://0.0.0.0%s", addr)
log.Printf("dags dir: %s", cfg.DagsDir)
log.Printf("database: %s", cfg.DBPath)
srv := &http.Server{Addr: addr, Handler: handler}
// Graceful shutdown.
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("shutting down...")
scheduler.Stop()
srv.Shutdown(context.Background())
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server: %v", err)
}
}
-30
View File
@@ -1,30 +0,0 @@
package main
import (
"log"
"net/http"
"time"
)
// corsMiddleware adds permissive CORS headers for development.
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// loggingMiddleware logs each HTTP request with method, path and duration.
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond))
})
}
-188
View File
@@ -1,188 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"fn-registry/functions/core"
"fn-registry/functions/infra"
)
// ScheduledDAG represents a DAG with a parsed cron schedule.
type ScheduledDAG struct {
Name string `json:"name"`
Path string `json:"path"`
Schedule string `json:"schedule"`
NextRun time.Time `json:"next_run"`
}
// Scheduler manages cron-triggered DAG execution.
type Scheduler struct {
mu sync.Mutex
running bool
cancel context.CancelFunc
dagsDir string
executor *Executor
dags []ScheduledDAG
}
// NewScheduler creates a new scheduler.
func NewScheduler(executor *Executor, dagsDir string) *Scheduler {
return &Scheduler{
executor: executor,
dagsDir: dagsDir,
}
}
// Start scans for DAGs with schedules and starts cron tickers for each.
func (s *Scheduler) Start() error {
s.mu.Lock()
if s.running {
s.mu.Unlock()
return fmt.Errorf("scheduler already running")
}
ctx, cancel := context.WithCancel(context.Background())
s.cancel = cancel
s.running = true
s.mu.Unlock()
scheduled, err := s.scanDAGs()
if err != nil {
s.mu.Lock()
s.running = false
s.mu.Unlock()
cancel()
return err
}
s.mu.Lock()
s.dags = scheduled
s.mu.Unlock()
log.Printf("[scheduler] started with %d DAGs", len(scheduled))
for _, dag := range scheduled {
dag := dag
go s.runTicker(ctx, dag)
}
return nil
}
// Stop cancels all tickers and stops the scheduler.
func (s *Scheduler) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
if !s.running {
return
}
s.cancel()
s.running = false
s.dags = nil
log.Printf("[scheduler] stopped")
}
// IsRunning returns true if the scheduler is active.
func (s *Scheduler) IsRunning() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.running
}
// Status returns the list of scheduled DAGs with their next run time.
type SchedulerStatus struct {
Running bool `json:"running"`
DAGs []ScheduledDAG `json:"dags"`
}
func (s *Scheduler) Status() SchedulerStatus {
s.mu.Lock()
defer s.mu.Unlock()
return SchedulerStatus{
Running: s.running,
DAGs: s.dags,
}
}
// scanDAGs reads the dags directory and returns DAGs that have cron schedules.
func (s *Scheduler) scanDAGs() ([]ScheduledDAG, error) {
entries, err := os.ReadDir(s.dagsDir)
if err != nil {
return nil, err
}
var scheduled []ScheduledDAG
for _, entry := range entries {
ext := filepath.Ext(entry.Name())
if ext != ".yaml" && ext != ".yml" {
continue
}
path := filepath.Join(s.dagsDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
continue
}
dag, err := core.DagParse(data)
if err != nil {
continue
}
for _, expr := range dag.Schedule {
sched, err := core.ParseCronExpr(strings.TrimSpace(expr))
if err != nil {
log.Printf("[scheduler] invalid cron %q in %s: %v", expr, dag.Name, err)
continue
}
next := core.NextCronTime(sched, time.Now())
scheduled = append(scheduled, ScheduledDAG{
Name: dag.Name,
Path: path,
Schedule: expr,
NextRun: next,
})
}
}
return scheduled, nil
}
// runTicker starts a cron ticker for a single DAG schedule.
func (s *Scheduler) runTicker(ctx context.Context, dag ScheduledDAG) {
sched, err := core.ParseCronExpr(strings.TrimSpace(dag.Schedule))
if err != nil {
return
}
// Convert core.CronSchedule to infra.CronTickerSchedule.
tickerSched := infra.CronTickerSchedule{
Minute: sched.Minute,
Hour: sched.Hour,
DayOfMonth: sched.DayOfMonth,
Month: sched.Month,
DayOfWeek: sched.DayOfWeek,
}
ch := infra.CronTicker(tickerSched, ctx)
log.Printf("[scheduler] ticker started for %s (%s), next: %s", dag.Name, dag.Schedule, dag.NextRun.Format(time.RFC3339))
for t := range ch {
log.Printf("[scheduler] triggered %s at %s", dag.Name, t.Format(time.RFC3339))
go func() {
runID, err := s.executor.ExecuteDAG(ctx, dag.Path, "cron")
if err != nil {
log.Printf("[scheduler] %s failed: %v (run: %s)", dag.Name, err, runID)
} else {
log.Printf("[scheduler] %s completed (run: %s)", dag.Name, runID)
}
}()
}
}
@@ -1,29 +0,0 @@
CREATE TABLE IF NOT EXISTS dag_runs (
id TEXT PRIMARY KEY,
dag_name TEXT NOT NULL,
dag_path TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','success','failed','cancelled')),
trigger TEXT NOT NULL DEFAULT 'manual' CHECK(trigger IN ('manual','cron','api')),
started_at TEXT NOT NULL,
finished_at TEXT,
error TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS dag_step_results (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL REFERENCES dag_runs(id) ON DELETE CASCADE,
step_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','success','failed','skipped')),
exit_code INTEGER NOT NULL DEFAULT -1,
stdout TEXT NOT NULL DEFAULT '',
stderr TEXT NOT NULL DEFAULT '',
started_at TEXT,
finished_at TEXT,
duration_ms INTEGER NOT NULL DEFAULT 0,
error TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_runs_dag_name ON dag_runs(dag_name);
CREATE INDEX IF NOT EXISTS idx_runs_status ON dag_runs(status);
CREATE INDEX IF NOT EXISTS idx_runs_started ON dag_runs(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_step_results_run ON dag_step_results(run_id);
-266
View File
@@ -1,266 +0,0 @@
package store
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"sort"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// applyMigrations executes every embedded migrations/*.sql in order.
// Each statement is idempotent (IF NOT EXISTS / ADD COLUMN). Duplicate-column
// errors from re-running ALTER TABLE ADD COLUMN are tolerated.
func applyMigrations(conn *sql.DB) error {
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
if err != nil {
return err
}
sort.Strings(files)
for _, f := range files {
b, err := migrationsFS.ReadFile(f)
if err != nil {
return fmt.Errorf("%s: read: %w", f, err)
}
if _, err := conn.Exec(string(b)); err != nil {
if strings.Contains(err.Error(), "duplicate column") ||
strings.Contains(err.Error(), "already exists") {
continue
}
return fmt.Errorf("%s: %w", f, err)
}
}
return nil
}
// DB wraps a SQLite connection for DAG run persistence.
type DB struct {
conn *sql.DB
path string
}
// Open opens or creates a DAG engine database at the given path.
func Open(path string) (*DB, error) {
conn, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on")
if err != nil {
return nil, fmt.Errorf("store: open %s: %w", path, err)
}
if err := applyMigrations(conn); err != nil {
conn.Close()
return nil, fmt.Errorf("store: migrate: %w", err)
}
return &DB{conn: conn, path: path}, nil
}
// Close closes the database connection.
func (db *DB) Close() error {
return db.conn.Close()
}
// Conn exposes the underlying *sql.DB for read-only queries from other
// packages (e.g. WS hub in events.go). Do not Close() the returned conn.
func (db *DB) Conn() *sql.DB {
return db.conn
}
// --- DagRun CRUD ---
// DagRun mirrors infra.DagRun for the store layer.
type DagRun struct {
ID string `json:"id"`
DagName string `json:"dag_name"`
DagPath string `json:"dag_path"`
Status string `json:"status"`
Trigger string `json:"trigger"`
StartedAt time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
Error string `json:"error,omitempty"`
}
// CreateRun inserts a new run record.
func (db *DB) CreateRun(run *DagRun) error {
_, err := db.conn.Exec(
`INSERT INTO dag_runs (id, dag_name, dag_path, status, trigger, started_at, error)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
run.ID, run.DagName, run.DagPath, run.Status, run.Trigger,
run.StartedAt.Format(time.RFC3339), run.Error,
)
return err
}
// UpdateRunStatus updates a run's status and optionally its finished_at and error.
func (db *DB) UpdateRunStatus(id, status string, finishedAt *time.Time, errMsg string) error {
var fin *string
if finishedAt != nil {
s := finishedAt.Format(time.RFC3339)
fin = &s
}
_, err := db.conn.Exec(
`UPDATE dag_runs SET status=?, finished_at=?, error=? WHERE id=?`,
status, fin, errMsg, id,
)
return err
}
// GetRun retrieves a single run by ID.
func (db *DB) GetRun(id string) (*DagRun, error) {
row := db.conn.QueryRow(
`SELECT id, dag_name, dag_path, status, trigger, started_at, finished_at, error
FROM dag_runs WHERE id=?`, id,
)
return scanRun(row)
}
// ListRuns returns runs, newest first, with optional dag name filter.
func (db *DB) ListRuns(dagName string, limit, offset int) ([]DagRun, int, error) {
var total int
var args []interface{}
where := ""
if dagName != "" {
where = " WHERE dag_name=?"
args = append(args, dagName)
}
err := db.conn.QueryRow("SELECT COUNT(*) FROM dag_runs"+where, args...).Scan(&total)
if err != nil {
return nil, 0, err
}
query := "SELECT id, dag_name, dag_path, status, trigger, started_at, finished_at, error FROM dag_runs" +
where + " ORDER BY started_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.conn.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var runs []DagRun
for rows.Next() {
r, err := scanRunRows(rows)
if err != nil {
return nil, 0, err
}
runs = append(runs, *r)
}
return runs, total, rows.Err()
}
// --- DagStepResult CRUD ---
// DagStepResult mirrors infra.DagStepResult for the store layer.
type DagStepResult struct {
ID string `json:"id"`
RunID string `json:"run_id"`
StepName string `json:"step_name"`
FunctionID string `json:"function_id,omitempty"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
// InsertStepResult inserts a new step result.
func (db *DB) InsertStepResult(r *DagStepResult) error {
var startedAt, finishedAt *string
if r.StartedAt != nil {
s := r.StartedAt.Format(time.RFC3339)
startedAt = &s
}
if r.FinishedAt != nil {
s := r.FinishedAt.Format(time.RFC3339)
finishedAt = &s
}
_, err := db.conn.Exec(
`INSERT INTO dag_step_results (id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.FunctionID, r.Status, r.ExitCode, r.Stdout, r.Stderr,
startedAt, finishedAt, r.DurationMs, r.Error,
)
return err
}
// UpdateStepResult updates a step result by ID.
func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr string, finishedAt *time.Time, durationMs int64, errMsg string) error {
var fin *string
if finishedAt != nil {
s := finishedAt.Format(time.RFC3339)
fin = &s
}
_, err := db.conn.Exec(
`UPDATE dag_step_results SET status=?, exit_code=?, stdout=?, stderr=?, finished_at=?, duration_ms=?, error=? WHERE id=?`,
status, exitCode, stdout, stderr, fin, durationMs, errMsg, id,
)
return err
}
// ListStepResults returns all step results for a given run.
func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
rows, err := db.conn.Query(
`SELECT id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var results []DagStepResult
for rows.Next() {
var r DagStepResult
var startedAt, finishedAt sql.NullString
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.FunctionID, &r.Status, &r.ExitCode,
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
return nil, err
}
if startedAt.Valid {
t, _ := time.Parse(time.RFC3339, startedAt.String)
r.StartedAt = &t
}
if finishedAt.Valid {
t, _ := time.Parse(time.RFC3339, finishedAt.String)
r.FinishedAt = &t
}
results = append(results, r)
}
return results, rows.Err()
}
// --- scan helpers ---
type scanner interface {
Scan(dest ...interface{}) error
}
func scanRun(s scanner) (*DagRun, error) {
var r DagRun
var startedAt string
var finishedAt sql.NullString
if err := s.Scan(&r.ID, &r.DagName, &r.DagPath, &r.Status, &r.Trigger, &startedAt, &finishedAt, &r.Error); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
r.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
if finishedAt.Valid {
t, _ := time.Parse(time.RFC3339, finishedAt.String)
r.FinishedAt = &t
}
return &r, nil
}
func scanRunRows(rows *sql.Rows) (*DagRun, error) {
return scanRun(rows)
}
-118
View File
@@ -1,118 +0,0 @@
---
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:
# gfx
- gl_loader_cpp_gfx
- gl_shader_cpp_gfx
- gl_framebuffer_cpp_gfx
- fullscreen_quad_cpp_gfx
- shader_canvas_cpp_gfx
- uniform_parser_cpp_gfx
- uniform_panel_cpp_gfx
- dag_catalog_cpp_gfx
- dag_compile_cpp_gfx
- dag_uniforms_cpp_gfx
- dag_panel_cpp_gfx
- dag_node_editor_cpp_gfx
- dag_palette_cpp_gfx
- dag_node_previews_cpp_gfx
- shaderlab_db_cpp_gfx
- code_to_generator_cpp_gfx
# core (modal Save-as-generator)
- modal_dialog_cpp_core
- text_input_cpp_core
- button_cpp_core
uses_types:
- dag_types_cpp_gfx
framework: "imgui"
entry_point: "main.cpp"
dir_path: "apps/shaders_lab"
repo_url: ""
icon:
phosphor: "palette"
accent: "#ea580c"
---
## Arquitectura
App ImGui de live-coding GLSL con dos modos en paralelo:
1. **Code panel** — editor de fragment shader libre. Las anotaciones en
uniforms (`// @slider`, `// @color`, `// @xy`, `// @toggle`) se parsean y
convierten en controles del panel **Controls** que escriben en un
`UniformStore` aplicado al programa cada frame.
2. **DAG panel** — pipeline node-based con catalogo de generadores
(plasma, voronoi, etc.) y filtros (blur, threshold, etc.) que se
compilan a un fragment shader unificado y se renderizan en **Canvas DAG**.
Al guardar un Code shader como "generator" se traduce a un `DagNodeDef` y se
persiste en `shaders_lab.db` (tabla via `shaderlab_db`), apareciendo en la
paleta del DAG junto a los builtins.
## Capas
| Archivo | Responsabilidad |
|---|---|
| `main.cpp` | UI shell, paneles, modal save-as, layouts, AppConfig |
| `compiler.cpp` | `compile_code()`, `compile_dag()`, `mark_code_dirty()` con debounce 250ms |
`main.cpp` mantiene estado global de sesion (g_source, g_pipeline, g_descs,
g_store, g_layouts...) — ImGui retained-mode obliga a que persista entre
frames. Toda la logica pura de compilacion vive en `compiler.cpp` y en las
funciones `dag_compile`, `code_to_generator`, `uniform_parser` del registry.
## Persistencia
- **`shaders_lab.db`** (junto al .exe) — tabla de generators de usuario via
`shaderlab_db_*`, ademas de `imgui_layouts` (creada por `layout_storage`).
- `imgui.ini` y `app_settings.ini` — gestionados por `fn::run_app` en
`<exe_dir>/local_files/`.
## Paneles
| Panel | Atajo | Que muestra |
|---|---|---|
| Code | Ctrl+1 | Editor del fragment shader + boton "Save as generator" |
| DAG Pipeline | Ctrl+2 | Node editor con la pipeline |
| Canvas Code | Ctrl+3 | Render del Code shader |
| Canvas DAG | Ctrl+4 | Render del shader compilado del DAG |
| Controls | Ctrl+5 | Sliders/color pickers de uniforms anotados |
| Functions | Ctrl+6 | Paleta del DAG (generators + filters + output) |
| Generated GLSL | Ctrl+7 | GLSL final del DAG con uniforms baked como const array |
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target shaders_lab
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target shaders_lab
```
## Decisiones
- `init_gl_loader = true` (via `fn::run_app` por default cuando se enlaza
con OpenGL) — `shader_canvas`, `gl_shader`, `gl_framebuffer` llaman gl*.
- `viewports = true` — los Canvas se pueden arrastrar fuera del main.
- DAG default: arranca con un nodo "plasma" + "output" si la paleta los
encuentra; persiste el INI con `layout_storage`.
- 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.
Binary file not shown.
@@ -0,0 +1,80 @@
---
name: chrome_load_extensions
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "chrome_load_extensions [--port N] [--profile DIR] --ext PATH [--ext PATH ...] [--proxy URL] [--url URL]"
description: "Lanza Chrome con extensiones unpacked via --load-extension (WSL2→Windows chrome.exe, paths traducidos, join sin echo, setsid anti-exit-144). OJO: --load-extension SOLO funciona en Chrome for Testing/Chromium/Dev. En Chrome STABLE 138+ esta DESACTIVADO (feature DisableLoadExtensionCommandLineSwitch + bloqueo duro en 148) y carga 0 extensiones aunque el cmdline sea correcto. Para Chrome stable usar install via Web Store (1-clic, persiste en perfil) o enterprise policy ExtensionInstallForcelist (requiere HKLM/HKCU Policies escribible — denegado en maquinas gestionadas)."
tags: [chrome, cdp, browser, extensions, wsl2, navegator]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
params:
- name: "--port N"
desc: "Puerto de remote debugging CDP. Default: 9222."
- name: "--profile DIR"
desc: "Chrome user-data-dir. Acepta ruta Windows (C:\\...) o ruta WSL/Linux (se traduce via wslpath -w). Default: C:\\Users\\<USERNAME>\\AppData\\Local\\fn-chrome-cdp-profile (WSL2) o /tmp/fn-chrome-cdp-profile (Linux nativo)."
- name: "--ext PATH"
desc: "Ruta a un directorio de extensión unpacked. Repetible. Acepta ruta Windows (se pasa intacta) o ruta WSL/Linux (se traduce via wslpath -w). Obligatorio al menos uno."
- name: "--proxy URL"
desc: "Proxy opcional, ej. http://127.0.0.1:8889. Agrega --proxy-server=URL a Chrome."
- name: "--url URL"
desc: "URL inicial opcional para abrir con --new-window."
output: "PID del proceso Chrome lanzado (stdout). Mensajes de estado en stderr. CDP listo en 127.0.0.1:<port>."
file_path: "bash/functions/browser/chrome_load_extensions.sh"
---
## Ejemplo
```bash
source bash/functions/browser/chrome_load_extensions.sh
chrome_load_extensions \
--port 9222 \
--profile 'C:\Users\lucas\AppData\Local\fn-chrome-cdp-profile' \
--ext 'C:\Users\lucas\hls-dl-ext' \
--ext 'C:\Users\lucas\ubol' \
--proxy http://127.0.0.1:8889 \
--url https://www.gnularetro.cc/
```
Sin proxy ni URL, sólo extensiones:
```bash
source bash/functions/browser/chrome_load_extensions.sh
pid=$(chrome_load_extensions \
--ext '/home/lucas/dev/hls-dl-ext' \
--ext '/home/lucas/dev/ubol')
# Paths WSL traducidos automáticamente a Windows.
# CDP listo en 127.0.0.1:9222.
echo "Chrome PID: $pid"
```
## Cuando usarla
Cuando necesites Chrome CDP con extensiones unpacked cargadas (HLS downloader, uBlock Origin, extensiones en desarrollo) y `chrome_launch_go_browser` no sirve porque hardcodea `--disable-extensions`. WSL2→Windows. Ideal para sesiones de navegator con proxy + extensión activa.
## Gotchas
- **MUERTO en Chrome STABLE 138+ (validado 2026-05-30, Chrome 148)**: `--load-extension` NO carga nada en el canal stable, ni con `--disable-extensions-except` ni con `--disable-features=DisableLoadExtensionCommandLineSwitch`. `chrome://version` muestra el flag correcto pero `chrome://extensions` sale vacío. Google lo bloqueó duro en stable. La función SOLO sirve en **Chrome for Testing / Chromium / Dev/Canary**, donde el switch sigue activo. Para stable: ver opciones abajo.
- **Instalar en Chrome STABLE (las que SÍ funcionan)**:
1. **Web Store 1-clic** — abre la página del store en el perfil CDP, el humano da "Añadir a Chrome". Persiste en el perfil para siempre (futuros lanzamientos ya con la extensión, sin flags). El popup de confirmación es UI del navegador (no DOM) → NO es CDP-clickable, requiere gesto humano. Único método no-admin que persiste por-perfil.
2. **Enterprise policy** `ExtensionInstallForcelist` (HKCU/HKLM `\Software\Policies\Google\Chrome`) — force-install sin clic desde el store, browser-wide. El key `Policies\Google\Chrome` puede dar "Access denied" al escribir (visto 2026-05-30 incluso en máquina personal vía reg.exe/PowerShell desde WSL — Chrome/Windows protege el subárbol Policies). Si funciona, requiere relanzar Chrome para que descargue del store. Método global (afecta todos los perfiles).
3. Extensiones **unpacked custom** (no en store, ej. un HLS downloader propio) en stable: no hay vía no-admin. Empaquetar a CRX + self-host `update_url` + policy, o usar Chrome for Testing. A menudo innecesario si la lógica vive fuera (ej. `grab_stream.py` descarga sin extensión).
- **Combo flags (solo Chrome for Testing/dev)**: requiere AMBOS `--load-extension=p1,p2` Y `--disable-extensions-except=p1,p2` juntos + `--disable-features=DisableLoadExtensionCommandLineSwitch`. **NUNCA `--disable-extensions`** (desactiva todo).
- **join sin `echo`**: rutas Windows `C:\Users\...` tienen `\U`; el `echo` de zsh (o sh con xpg_echo) lo interpreta como escape unicode y trunca la ruta a `C:`. La función usa acumulador `+=`, no `echo`. Verificable en `chrome://version` (debe verse el path completo, no `--load-extension=C:`).
- **exit 144 en Bash tool**: si el proceso Chrome retiene el pipe stdout, la herramienta devuelve exit 144. Esta función lanza con `setsid ... </dev/null >log 2>&1 &` + `disown` para desacoplar completamente. El log queda en `/tmp/chrome_ext_<port>.log`.
- **WSL2: traducir paths con `wslpath -w`**: los paths de `--ext` y `--profile` que sean rutas Linux se traducen automáticamente. Las rutas Windows (`C:\...`) se pasan intactas. `wslpath` debe estar disponible (estándar en WSL2 desde Windows 10 1903+).
- **Perfil ya abierto**: si Chrome ya tiene ese perfil abierto, relanzar añade una ventana extra a la misma instancia. La función detecta si CDP ya responde en el puerto y avisa por stderr, pero procede igualmente.
- **Web Store vs unpacked**: instalar extensiones desde la Web Store (un clic) persiste en el perfil sin necesidad de flags y sobrevive reinicios. Esta función es para extensiones unpacked en desarrollo o que no están en la Web Store. Si usas ambas, los flags no interfieren con las instaladas del store.
- **zsh globbing**: `--remote-allow-origins=*` está dentro de comillas en la función, no se expande. Si lo pasas desde la línea de comandos, entrecomillarlo.
- **Proxy + extensión**: si usas proxy para captura de tráfico (Burp, mitmproxy, gost), el proxy se aplica a toda la sesión Chrome, incluyendo el tráfico de las extensiones.
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# chrome_load_extensions — lanza Chrome (WSL2→Windows chrome.exe) con extensiones unpacked cargadas en un perfil CDP.
# Chrome 148+: requiere --load-extension=<paths> Y --disable-extensions-except=<same paths> juntos.
# NUNCA pasar --disable-extensions (desactiva todo, incluyendo las que quieres cargar).
chrome_load_extensions() {
local port=9222
local profile=""
local proxy=""
local url=""
local -a ext_paths=()
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--port)
port="$2"; shift 2 ;;
--profile)
profile="$2"; shift 2 ;;
--ext)
ext_paths+=("$2"); shift 2 ;;
--proxy)
proxy="$2"; shift 2 ;;
--url)
url="$2"; shift 2 ;;
--*)
echo "chrome_load_extensions: flag desconocido: $1" >&2; return 1 ;;
*)
# Positional = extra ext path
ext_paths+=("$1"); shift ;;
esac
done
if [[ ${#ext_paths[@]} -eq 0 ]]; then
echo "chrome_load_extensions: se requiere al menos un --ext PATH de extension unpacked" >&2
return 1
fi
# --- Detectar chrome.exe ---
local chrome_bin=""
if command -v chrome.exe &>/dev/null; then
chrome_bin="chrome.exe"
elif [[ -f "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" ]]; then
chrome_bin="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
elif [[ -f "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" ]]; then
chrome_bin="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
else
echo "chrome_load_extensions: chrome.exe no encontrado en PATH ni en rutas conocidas" >&2
return 1
fi
# --- Detectar WSL2 ---
local wsl2=0
if grep -qi 'microsoft\|wsl' /proc/version 2>/dev/null; then
wsl2=1
fi
# --- Traducir paths de extensiones a Windows si hace falta ---
local -a win_ext_paths=()
for p in "${ext_paths[@]}"; do
if [[ $wsl2 -eq 1 ]] && [[ "$p" != [A-Za-z]:\\* ]]; then
# Path Linux → traducir a Windows
local win_p
win_p=$(wslpath -w "$p" 2>/dev/null) || {
echo "chrome_load_extensions: wslpath -w '$p' falló" >&2
return 1
}
win_ext_paths+=("$win_p")
else
win_ext_paths+=("$p")
fi
done
# --- Resolver perfil ---
if [[ -z "$profile" ]]; then
# Default: perfil canónico fn-chrome-cdp-profile en Windows
local win_user="${USERNAME:-${USER:-lucas}}"
if [[ $wsl2 -eq 1 ]]; then
profile="C:\\Users\\${win_user}\\AppData\\Local\\fn-chrome-cdp-profile"
else
profile="/tmp/fn-chrome-cdp-profile"
fi
elif [[ $wsl2 -eq 1 ]] && [[ "$profile" != [A-Za-z]:\\* ]]; then
# Path Linux del perfil → traducir a Windows
profile=$(wslpath -w "$profile" 2>/dev/null) || {
echo "chrome_load_extensions: wslpath -w '$profile' falló" >&2
return 1
}
fi
# --- Construir lista de paths separada por coma (para Chrome) ---
# Chrome usa coma como separador en --load-extension y --disable-extensions-except.
# NO usar `echo` para el join: rutas Windows como C:\Users tienen \U, y el echo de
# zsh (o sh con xpg_echo) interpreta \U como escape unicode y trunca la ruta a "C:".
# Acumulador con += y printf-safe, sin interpretacion de backslashes.
local ext_list=""
local p
for p in "${win_ext_paths[@]}"; do
ext_list+="${ext_list:+,}${p}"
done
# --- Construir args de Chrome ---
local -a args=(
"--remote-debugging-port=${port}"
"--user-data-dir=${profile}"
"--no-first-run"
"--no-default-browser-check"
"--remote-allow-origins=*"
"--load-extension=${ext_list}"
"--disable-extensions-except=${ext_list}"
# Chrome 137+ activa por defecto el feature DisableLoadExtensionCommandLineSwitch,
# que IGNORA silenciosamente --load-extension. Hay que desactivarlo o las
# extensiones unpacked no cargan (chrome://extensions sale vacio).
"--disable-features=DisableLoadExtensionCommandLineSwitch"
)
# WSL2: bind en 0.0.0.0 para que sea accesible desde la red WSL
if [[ $wsl2 -eq 1 ]]; then
args+=("--remote-debugging-address=0.0.0.0")
fi
if [[ -n "$proxy" ]]; then
args+=("--proxy-server=${proxy}")
fi
if [[ -n "$url" ]]; then
args+=("--new-window" "$url")
fi
# --- Revisar si CDP ya responde en el puerto ---
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
echo "chrome_load_extensions: CDP ya activo en puerto ${port}; lanzando ventana extra" >&2
fi
# --- Lanzar Chrome desacoplado del proceso padre ---
# setsid + redirección evita el exit 144 en el Bash tool (el pipe no queda retenido).
setsid "$chrome_bin" "${args[@]}" </dev/null >"/tmp/chrome_ext_${port}.log" 2>&1 &
local chrome_pid=$!
disown "$chrome_pid"
echo "chrome_load_extensions: Chrome lanzado PID=${chrome_pid} puerto=${port}" >&2
# --- Esperar a que CDP esté listo (hasta 15 segundos) ---
local deadline=$(( $(date +%s) + 15 ))
local ready=0
while [[ $(date +%s) -lt $deadline ]]; do
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
ready=1
break
fi
sleep 0.5
done
if [[ $ready -eq 1 ]]; then
echo "chrome_load_extensions: CDP listo en 127.0.0.1:${port}"
else
echo "chrome_load_extensions: advertencia — CDP no respondió en 15s en puerto ${port}; Chrome puede estar iniciando lentamente" >&2
fi
echo "$chrome_pid"
}
@@ -0,0 +1,81 @@
---
name: install_chromium_proxy_extension
kind: function
lang: bash
domain: browser
version: 1.0.0
purity: impure
signature: install_chromium_proxy_extension --ext-dir DIR [--name NAME] [--stable-dir DIR] [--uninstall]
description: "Instala una extension desempaquetada de Chromium en todos los perfiles del usuario de forma persistente, escribiendo un fragmento en /etc/chromium.d/ que el wrapper de Chromium carga en cada arranque. Pensado para distribuir la extension de toggle de proxy de web_proxy sin Web Store, pero sirve para cualquier extension desempaquetada."
tags: [web-proxy, chromium, extension, browser, proxy, install]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
params:
- name: --ext-dir
desc: "Directorio de la extension desempaquetada de origen (debe contener manifest.json). Obligatorio salvo en --uninstall."
- name: --name
desc: "Nombre del fragmento en /etc/chromium.d/ (default web_proxy_ext). Identifica esta instalacion para poder desinstalarla."
- name: --stable-dir
desc: "Ruta estable donde se copia la extension, independiente del repo (default ~/.web_proxy/extension). --load-extension apunta aqui."
- name: --uninstall
desc: "Elimina el fragmento de /etc/chromium.d/ y la copia estable. No requiere --ext-dir."
output: "JSON en stdout: {installed|uninstalled, name, stable_dir, chromiumd, ext_id}. Requiere sudo para escribir en /etc/chromium.d/."
file_path: bash/functions/browser/install_chromium_proxy_extension.sh
---
# install_chromium_proxy_extension
Instala una extension desempaquetada de Chromium en **todos los perfiles** del
usuario, de forma persistente, sin pasar por la Chrome Web Store.
## Ejemplo
```bash
# Instalar la extension de toggle de proxy de web_proxy en todos los perfiles
install_chromium_proxy_extension --ext-dir /home/enmanuel/fn_registry/apps/web_proxy/extension
# Desinstalarla
install_chromium_proxy_extension --uninstall
# Otra extension, con nombre y ruta estable propios
install_chromium_proxy_extension --ext-dir ~/mis-extensiones/foo --name foo_ext --stable-dir ~/.local/share/foo_ext
```
Tras instalar, cierra y vuelve a abrir Chromium: la extension aparece en todos
los perfiles, incluidos los que se creen despues.
## Cuando usarla
Cuando necesitas que una extension desempaquetada este presente en todos los
perfiles de Chromium de una maquina (por ejemplo, un toggle de proxy de captura
preconfigurado) y no quieres publicarla en la Web Store ni cargarla a mano en
cada perfil. Es la pieza que hace que `web_proxy` quede "a un clic" en cualquier
ventana de Chromium.
## Gotchas
- **Requiere sudo** para escribir en `/etc/chromium.d/`. Ten las credenciales
cacheadas (`sudo -v`) antes de invocarla de forma no interactiva.
- **Solo para el wrapper de Chromium de Debian/Ubuntu** (paquete `chromium`,
no snap ni Google Chrome). El wrapper hace `source /etc/chromium.d/*` en cada
arranque. Comprueba con `head -1 $(command -v chromium)` que es un script.
- **`--enable-remote-extensions` es imprescindible** en estos builds: sin el,
el wrapper anade `--disable-extensions-except` y `--disable-background-networking`,
que deshabilitan toda extension que no venga por `--load-extension`. El
fragmento generado lo incluye; por eso las demas extensiones del usuario
siguen funcionando.
- La extension se carga **desempaquetada** (`--load-extension`), no como `.crx`
firmado. Chromium puede mostrar un aviso de "extensiones en modo desarrollador".
El force-install via managed policy con `.crx` local + `update_url file://`
no funciona con este wrapper (lo bloquea `--disable-extensions-except`).
- El ID de la extension depende de `--stable-dir` (se deriva del path). Si
cambias la ruta estable, el ID cambia.
- No reinicia Chromium: los cambios aplican en el siguiente arranque del
navegador.
## Capability growth log
- v1.0.0 (2026-06-02) — version inicial. Instala/desinstala extension global via /etc/chromium.d con --enable-remote-extensions + --load-extension.
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# install_chromium_proxy_extension — instala una extension desempaquetada de
# Chromium en TODOS los perfiles del usuario, de forma persistente, escribiendo
# un fragmento en /etc/chromium.d/ que el wrapper de Chromium carga en cada
# arranque.
#
# Por que /etc/chromium.d en vez de managed policy con .crx force_installed:
# el wrapper de Chromium de Debian/Ubuntu (xtradeb y derivados), cuando NO se
# pasa --enable-remote-extensions, anade --disable-extensions-except=<solo las
# de --load-extension> y --disable-background-networking. Eso deshabilita
# cualquier extension force_installed por policy y bloquea su update check. La
# via fiable es habilitar --enable-remote-extensions y cargar la extension
# desempaquetada con --load-extension, ambos inyectados de forma global desde
# /etc/chromium.d/, que el wrapper hace `source` en cada lanzamiento.
install_chromium_proxy_extension() {
local ext_dir=""
local name="web_proxy_ext"
local stable_dir="$HOME/.web_proxy/extension"
local chromiumd="/etc/chromium.d"
local uninstall="no"
# Permite sudo no interactivo via SUDO_ASKPASS (sudo -A) cuando se ejecuta
# sin terminal (agentes, CI). Con terminal interactivo usa sudo normal.
local SUDO="sudo"
[[ -n "${SUDO_ASKPASS:-}" ]] && SUDO="sudo -A"
while [[ $# -gt 0 ]]; do
case "$1" in
--ext-dir) ext_dir="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--stable-dir) stable_dir="$2"; shift 2 ;;
--uninstall) uninstall="yes"; shift ;;
*) echo "ERROR: argumento desconocido: $1" >&2; return 1 ;;
esac
done
if [[ ! -d "$chromiumd" ]]; then
echo "ERROR: $chromiumd no existe. Este Chromium no usa el wrapper con /etc/chromium.d." >&2
echo " Comprueba 'head -1 \$(command -v chromium)'; si no es un wrapper shell, usa otra via." >&2
return 1
fi
# Desinstalacion: quitar el fragmento global y la copia estable.
if [[ "$uninstall" == "yes" ]]; then
$SUDO rm -f "${chromiumd}/${name}" || {
echo "ERROR: no se pudo eliminar ${chromiumd}/${name} (requiere sudo)." >&2
return 1
}
rm -rf "$stable_dir"
printf '{"uninstalled": true, "name": "%s"}\n' "$name"
return 0
fi
# Instalacion: validar la extension de origen.
if [[ -z "$ext_dir" || ! -f "${ext_dir}/manifest.json" ]]; then
echo "ERROR: --ext-dir debe apuntar a un directorio con manifest.json." >&2
return 1
fi
# Copiar la extension a una ubicacion estable, independiente del repo, para
# que --load-extension no se rompa si el repo se mueve o se limpia.
mkdir -p "$stable_dir" || {
echo "ERROR: no se pudo crear $stable_dir." >&2
return 1
}
# Vaciar destino y copiar el contenido del origen.
rm -rf "${stable_dir:?}/"* 2>/dev/null
cp -r "${ext_dir}/." "$stable_dir/" || {
echo "ERROR: no se pudo copiar la extension a $stable_dir." >&2
return 1
}
# Escribir el fragmento que el wrapper carga en cada arranque. Se hace via
# archivo temporal + sudo cp para no exponer el contenido por una tuberia.
local tmp
tmp="$(mktemp)"
printf 'export CHROMIUM_FLAGS="$CHROMIUM_FLAGS --enable-remote-extensions --load-extension=%s"\n' "$stable_dir" > "$tmp"
if ! $SUDO cp "$tmp" "${chromiumd}/${name}"; then
rm -f "$tmp"
echo "ERROR: no se pudo escribir ${chromiumd}/${name} (requiere sudo)." >&2
return 1
fi
$SUDO chmod 0644 "${chromiumd}/${name}" 2>/dev/null
rm -f "$tmp"
# ID de extension desempaquetada (deterministico: sha256 del path estable).
local ext_id
ext_id="$(python3 - "$stable_dir" <<'PY' 2>/dev/null
import hashlib, sys
h = hashlib.sha256(sys.argv[1].encode()).hexdigest()[:32]
print(''.join(chr(ord('a') + int(c, 16)) for c in h))
PY
)"
printf '{"installed": true, "name": "%s", "stable_dir": "%s", "chromiumd": "%s/%s", "ext_id": "%s"}\n' \
"$name" "$stable_dir" "$chromiumd" "$name" "$ext_id"
}
# Ejecutar si se llama directamente (fn run / bash <file>)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
install_chromium_proxy_extension "$@"
fi
@@ -0,0 +1,74 @@
---
name: launch_chromium_proxy
kind: function
lang: bash
domain: browser
version: "1.0.0"
purity: impure
signature: "launch_chromium_proxy [--proxy URL] [--profile DIR] [--url URL] [--ca-cert PATH] [--extra \"ARGS\"]"
description: "Lanza Chromium (o Chrome) apuntando a un proxy HTTP/HTTPS local con un perfil completamente aislado del perfil real del usuario. Pensado para capturar trafico con un proxy de interceptacion (mitmproxy, Burp Suite) sin contaminar la sesion normal de navegacion. Emite un JSON con el PID del proceso lanzado."
tags: [chromium, chrome, proxy, mitmproxy, burp, browser, web-proxy, intercept, tls]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--proxy URL"
desc: "URL del proxy HTTP/HTTPS local, ej. http://127.0.0.1:8080. Se pasa a Chromium como --proxy-server=URL. Default: http://127.0.0.1:8080."
- name: "--profile DIR"
desc: "Directorio de perfil aislado para Chromium (--user-data-dir). Se crea automaticamente si no existe. Default: /tmp/chromium-proxy. Usar un path distinto por sesion si se quiere aislamiento total entre corridas."
- name: "--url URL"
desc: "URL inicial a abrir al arrancar el navegador. Opcional. Si se omite, Chromium abre su pagina de nueva pestana."
- name: "--ca-cert PATH"
desc: "Ruta a un CA cert PEM del proxy (ej. ~/.mitmproxy/mitmproxy-ca-cert.pem). Si se pasa, la funcion NO agrega --ignore-certificate-errors y asume que el usuario ya importo el CA en el perfil o en el sistema. Si se omite, se agrega --ignore-certificate-errors automaticamente para que el proxy MITM no rompa HTTPS (menos seguro)."
- name: "--extra \"ARGS\""
desc: "Flags extra que se pasan directamente a chromium, ej. --extra \"--disable-gpu --window-size=1280,800\". El valor completo debe ir entre comillas."
output: "JSON en stdout: {\"pid\": <pid>, \"browser\": \"<binario>\", \"proxy\": \"<url>\", \"profile\": \"<dir>\", \"log\": \"<ruta_log>\"}. Mensajes de estado e informacion de CA en stderr. El navegador corre en background desacoplado de la sesion. Exit codes: 0=lanzado correctamente, 1=binario no encontrado o argumento invalido o error al crear el directorio de perfil."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/browser/launch_chromium_proxy.sh"
---
## Ejemplo
```bash
source bash/functions/browser/launch_chromium_proxy.sh
# Caso mas comun: interceptar trafico con mitmproxy corriendo en 8080
# (sin CA instalado: --ignore-certificate-errors se aplica automaticamente)
result=$(launch_chromium_proxy --proxy http://127.0.0.1:8080 --url https://httpbin.org/get)
echo "$result"
# {"pid":12345,"browser":"chromium","proxy":"http://127.0.0.1:8080","profile":"/tmp/chromium-proxy","log":"/tmp/chromium-proxy-12345.log"}
# Con CA cert instalado (mitmproxy): sin --ignore-certificate-errors
launch_chromium_proxy \
--proxy http://127.0.0.1:8080 \
--ca-cert ~/.mitmproxy/mitmproxy-ca-cert.pem \
--profile /tmp/mitm-session \
--url https://api.ejemplo.com/v1/test
# Con Burp Suite en puerto 8081, perfil aislado y ventana de tamano fijo
launch_chromium_proxy \
--proxy http://127.0.0.1:8081 \
--profile /tmp/burp-session \
--extra "--window-size=1440,900" \
--url https://app.objetivo.com
```
## Cuando usarla
Cuando necesitas capturar y analizar trafico HTTPS de un navegador con mitmproxy, Burp Suite u otro proxy de interceptacion, sin tocar el perfil real del usuario ni sus cookies/credenciales guardadas. Ideal antes de hacer analisis de trafico de una app web o API, o al reproducir un flujo autenticado desde una sesion limpia.
## Gotchas
- **Deteccion de binario en orden**: la funcion prueba `chromium`, `chromium-browser`, `google-chrome-stable`, `google-chrome`. En sistemas donde solo existe `google-chrome`, ese sera el binario usado. Si ninguno esta en el PATH, retorna exit 1. Instalar con `sudo apt install chromium` o `chromium-browser`.
- **`--ignore-certificate-errors` sin `--ca-cert`**: este flag desactiva toda la validacion TLS del navegador. Es conveniente para empezar rapido, pero reduce la seguridad de la sesion. Para produccion o analisis de seguridad serio, instalar el CA del proxy en el sistema (`sudo cp mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt && sudo update-ca-certificates`) o en el perfil de Chromium (chrome://settings/certificates), y pasar `--ca-cert` para que la funcion omita el flag inseguro.
- **`--proxy-bypass-list="<-loopback>"`**: fuerza que el trafico loopback (127.0.0.1, localhost) TAMBIEN pase por el proxy. Sin esto, Chromium excluye loopback del proxy por defecto y no veras esas peticiones en mitmproxy. Si quieres el comportamiento estandar (excluir loopback), elimina este flag via `--extra`.
- **Perfil persistente entre sesiones**: el perfil en `/tmp/chromium-proxy` (o el directorio que elijas) persiste entre ejecuciones. Si quieres una sesion 100% limpia cada vez, pasa `--profile /tmp/chromium-proxy-$$` (usa el PID del shell como sufijo) o borra el directorio antes de llamar a la funcion.
- **`setsid` + `disown`**: el navegador se lanza desacoplado de la sesion del agente. Si la shell/sesion que llamo a la funcion termina, el proceso Chromium sigue vivo. Para matarlo, usar `kill <pid>` con el PID del JSON de salida.
- **Log del navegador**: stdout y stderr de Chromium se redirigen a `/tmp/chromium-proxy-<pid>.log`. Si el navegador no arranca, revisar ese archivo para ver el error.
- **Chrome STABLE 138+**: al igual que `chrome_load_extensions`, algunos flags de automatizacion estan bloqueados en Chrome STABLE. Para interceptacion de trafico `--proxy-server` y `--user-data-dir` siguen funcionando en todas las versiones. Esta funcion es compatible con Chrome/Chromium en cualquier canal.
- **Multiples instancias**: si ya hay una instancia de Chromium corriendo con el mismo `--user-data-dir`, Chromium puede reusar esa instancia en lugar de abrir una nueva. Usar un directorio de perfil distinto por sesion concurrente.
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# launch_chromium_proxy — Lanza Chromium apuntando a un proxy HTTP/HTTPS local con perfil aislado.
launch_chromium_proxy() {
local proxy_url="http://127.0.0.1:8080"
local profile_dir="/tmp/chromium-proxy"
local start_url=""
local ca_cert=""
local extra_args=""
local ext_dir=""
# Parsear argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--proxy)
proxy_url="$2"; shift 2 ;;
--profile)
profile_dir="$2"; shift 2 ;;
--url)
start_url="$2"; shift 2 ;;
--ca-cert)
ca_cert="$2"; shift 2 ;;
--ext)
ext_dir="$2"; shift 2 ;;
--extra)
extra_args="$2"; shift 2 ;;
*)
echo "ERROR: argumento desconocido: $1" >&2
return 1 ;;
esac
done
# Detectar binario del navegador
local browser_bin=""
for candidate in chromium chromium-browser google-chrome-stable google-chrome; do
if command -v "$candidate" &>/dev/null; then
browser_bin="$candidate"
break
fi
done
if [[ -z "$browser_bin" ]]; then
echo "ERROR: no se encontro ningun binario Chromium/Chrome en el PATH." >&2
echo " Probados: chromium, chromium-browser, google-chrome-stable, google-chrome." >&2
return 1
fi
# Crear directorio de perfil si no existe
if [[ ! -d "$profile_dir" ]]; then
mkdir -p "$profile_dir" || {
echo "ERROR: no se pudo crear el directorio de perfil: $profile_dir" >&2
return 1
}
fi
# Construir argumentos del navegador
local args=(
"--user-data-dir=${profile_dir}"
"--no-first-run"
"--no-default-browser-check"
)
# Proxy fijo opcional. Con "--proxy none" (o vacio) no se fija proxy en el
# cmdline: util cuando una extension de proxy gestiona la conexion (toggle).
if [[ -n "$proxy_url" && "$proxy_url" != "none" ]]; then
args+=("--proxy-server=${proxy_url}" "--proxy-bypass-list=<-loopback>")
fi
# Cargar una extension desempaquetada (--load-extension). Funciona en
# Chromium (no en Chrome stable 138+). Para persistencia en todos los
# perfiles se usa managed policy en su lugar.
if [[ -n "$ext_dir" ]]; then
args+=("--load-extension=${ext_dir}")
fi
# Manejo de certificados TLS
if [[ -n "$ca_cert" ]]; then
# El usuario instalo el CA en el perfil; no ignorar errores de certificado.
# (El CA se instala en el sistema o en el perfil antes de lanzar.)
echo "INFO: CA cert declarado: $ca_cert" >&2
echo "INFO: Asegurate de haber importado el CA en el perfil o en el sistema antes de navegar HTTPS." >&2
else
# Sin CA cert: ignorar errores de certificado para que mitmproxy/Burp funcionen sin configuracion extra.
# ADVERTENCIA: esto desactiva la validacion TLS completa del navegador.
args+=("--ignore-certificate-errors")
echo "WARN: --ignore-certificate-errors activo. Usa --ca-cert si instalaste el CA del proxy." >&2
fi
# URL inicial opcional
if [[ -n "$start_url" ]]; then
args+=("$start_url")
fi
# Argumentos extra pasados por el usuario
# shellcheck disable=SC2206
local extra_arr=()
if [[ -n "$extra_args" ]]; then
read -r -a extra_arr <<< "$extra_args"
args+=("${extra_arr[@]}")
fi
# Log temporal para stderr/stdout del navegador
local log_file="/tmp/chromium-proxy-$$.log"
# Lanzar en background desacoplado de la sesion del agente
setsid "$browser_bin" "${args[@]}" </dev/null >"$log_file" 2>&1 &
local browser_pid=$!
disown "$browser_pid"
# Emitir JSON con informacion del proceso lanzado
printf '{"pid":%d,"browser":"%s","proxy":"%s","profile":"%s","log":"%s"}\n' \
"$browser_pid" "$browser_bin" "$proxy_url" "$profile_dir" "$log_file"
}
# Ejecutar si se llama directamente (fn run / bash <file>)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
launch_chromium_proxy "$@"
fi
@@ -0,0 +1,88 @@
---
name: query_mitm_flows
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "query_mitm_flows(file_or_glob: string, filter?: string, har?: string, mitmdump?: string) -> void"
description: "Consulta capturas .mitm guardadas con mitmproxy: vuelca los flujos que coinciden con un filtro de mitmproxy a stdout, o exporta la captura a HAR. Acepta uno o varios archivos .mitm (incluyendo globs expandidos por el shell). Autodetecta mitmdump en PATH y $HOME/.local/bin."
tags: [bash, cybersecurity, mitmproxy, web-proxy, query, har, capture, traffic, proxy, network]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: file_or_glob
desc: "Ruta a un archivo .mitm o glob expandido por el shell (ej. ~/captures/traffic-*.mitm). Acepta múltiples archivos como argumentos posicionales."
- name: filter
desc: "Expresión de filtro mitmproxy (ej. '~m POST & ~u /api', '~c 500', '~d example.com'). Si se omite, vuelca todos los flujos."
- name: har
desc: "Ruta de salida para exportar en formato HAR (--set hardump=OUT). Si se omite, el volcado va a stdout."
- name: mitmdump
desc: "Ruta al binario mitmdump. Si se omite, autodetecta en PATH y luego en $HOME/.local/bin/mitmdump."
output: "Vuelca los flujos capturados a stdout (modo default) o exporta a un archivo HAR (con --har); informa la ruta de exportación en stderr al terminar. Exit code de mitmdump propagado."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/query_mitm_flows.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/query_mitm_flows.sh
# Volcar todos los flujos de una captura
query_mitm_flows ~/captures/traffic-20260602.mitm
# Filtrar solo peticiones POST a /api (glob expandido por el shell)
query_mitm_flows ~/captures/traffic-20260602-*.mitm --filter "~m POST & ~u /api"
# Ver solo respuestas con código 500
query_mitm_flows session.mitm --filter "~c 500"
# Exportar a HAR para abrir en el Network tab del browser DevTools
query_mitm_flows ~/captures/traffic-20260602.mitm --har salida.har
# Exportar a HAR con filtro aplicado
query_mitm_flows session.mitm --filter "~d example.com" --har example_flows.har
# Especificar mitmdump manualmente (ej. en un venv de Python)
query_mitm_flows session.mitm --mitmdump ~/.venv/bin/mitmdump --filter "~m POST"
```
## Cuando usarla
Cuando hayas capturado tráfico HTTP/HTTPS con mitmproxy y necesites consultarlo después sin abrir una interfaz interactiva: filtrar flujos específicos por método, dominio, URL o código de respuesta, o exportar la captura a HAR para análisis posterior en herramientas externas (browser DevTools, Insomnia, Postman). Úsala desde scripts de análisis automatizados o pipelines de revisión de seguridad.
## Gotchas
**Sintaxis de filtros mitmproxy** — los filtros se pasan como expresión entre comillas:
| Operador | Significado | Ejemplo |
|---|---|---|
| `~u <regex>` | URL (path + query) | `~u /api/login` |
| `~d <regex>` | Dominio del host | `~d example.com` |
| `~m <método>` | Método HTTP | `~m POST` |
| `~c <código>` | Código de respuesta | `~c 500` |
| `~bq <regex>` | Body del request | `~bq password` |
| `~bs <regex>` | Body del response | `~bs token` |
| `~t <regex>` | Content-Type | `~t application/json` |
| `~s` | Solo respuestas | `~s` |
| `~q` | Solo requests | `~q` |
Combinar con `&` (AND), `|` (OR), `!` (NOT). Ejemplos:
- `"~m POST & ~u /api"` — POST a rutas /api
- `"~c 500 | ~c 503"` — errores de servidor
- `"~d example.com & !~u /static"` — todo de example.com excepto estáticos
**Globs:** el shell expande el glob *antes* de pasar los argumentos a la función. Pasar `~/captures/traffic-*.mitm` sin comillas para que el shell expanda; con comillas el glob llega literalmente y fallará si el archivo no existe.
**Inspección interactiva:** para navegar los flujos con UI, usar `mitmweb -r <file>` (web) o `mitmproxy -r <file>` (TUI), no esta función. Esta función es para consulta programática y scripting.
**Múltiples archivos con `-r`:** mitmdump acepta varios flags `-r` y concatena los flujos en orden. El filtro se aplica sobre el conjunto completo.
**HAR y filtros juntos:** al usar `--har` con `--filter`, solo los flujos que pasen el filtro se exportan al HAR.
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# query_mitm_flows — Consulta capturas .mitm con mitmdump: vuelca flujos a stdout
# o exporta a HAR. Acepta uno o varios archivos (también globs expandidos por el shell).
query_mitm_flows() {
local -a files=()
local filter=""
local har_out=""
local mitmdump_bin=""
# ── Parseo de argumentos ────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--filter)
[[ -z "${2:-}" ]] && { echo "ERROR: --filter requiere un valor" >&2; return 1; }
filter="$2"; shift 2 ;;
--har)
[[ -z "${2:-}" ]] && { echo "ERROR: --har requiere una ruta de salida" >&2; return 1; }
har_out="$2"; shift 2 ;;
--mitmdump)
[[ -z "${2:-}" ]] && { echo "ERROR: --mitmdump requiere la ruta al binario" >&2; return 1; }
mitmdump_bin="$2"; shift 2 ;;
--*)
echo "ERROR: opcion desconocida: $1" >&2; return 1 ;;
*)
files+=("$1"); shift ;;
esac
done
# ── Validar que se paso al menos un archivo ─────────────────────────────────
if [[ ${#files[@]} -eq 0 ]]; then
echo "ERROR: se requiere al menos un archivo .mitm como argumento" >&2
echo "Uso: query_mitm_flows <file_or_glob> [--filter EXPR] [--har OUT] [--mitmdump BIN]" >&2
return 1
fi
# ── Verificar que cada archivo existe ──────────────────────────────────────
local -a valid_files=()
for f in "${files[@]}"; do
if [[ -f "$f" ]]; then
valid_files+=("$f")
else
echo "ERROR: archivo no encontrado: $f" >&2
fi
done
if [[ ${#valid_files[@]} -eq 0 ]]; then
echo "ERROR: ningun archivo valido encontrado (el patron no matcheo nada)" >&2
return 1
fi
# ── Autodetectar mitmdump ───────────────────────────────────────────────────
if [[ -z "$mitmdump_bin" ]]; then
if command -v mitmdump &>/dev/null; then
mitmdump_bin="mitmdump"
elif [[ -x "$HOME/.local/bin/mitmdump" ]]; then
mitmdump_bin="$HOME/.local/bin/mitmdump"
else
echo "ERROR: mitmdump no encontrado. Instala mitmproxy:" >&2
echo " pip install mitmproxy o pip install --user mitmproxy" >&2
return 1
fi
fi
if [[ ! -x "$(command -v "$mitmdump_bin" 2>/dev/null || echo "$mitmdump_bin")" ]]; then
echo "ERROR: binario mitmdump no ejecutable: $mitmdump_bin" >&2
return 1
fi
# ── Construir y ejecutar el comando ────────────────────────────────────────
local -a cmd=("$mitmdump_bin" -n)
# Añadir todos los archivos de entrada con -r
for f in "${valid_files[@]}"; do
cmd+=(-r "$f")
done
# Modo HAR
if [[ -n "$har_out" ]]; then
cmd+=(--set "hardump=${har_out}")
fi
# Filtro de flujos (argumento posicional al final)
if [[ -n "$filter" ]]; then
cmd+=("$filter")
fi
"${cmd[@]}"
local exit_code=$?
if [[ -n "$har_out" && $exit_code -eq 0 ]]; then
echo "HAR exportado a ${har_out}" >&2
fi
return $exit_code
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
query_mitm_flows "$@"
fi
@@ -38,7 +38,7 @@ if [[ -n "$matches" ]]; then
fi
# Escanear repo especifico
scan_secrets_in_dirty /home/lucas/fn_registry
scan_secrets_in_dirty $HOME/fn_registry
```
## Patrones detectados
@@ -0,0 +1,80 @@
---
name: start_mitm_capture
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "start_mitm_capture([--port N] [--out DIR] [--rotate-min N] [--addon PATH] [--mitmdump BIN] [--log PATH]) -> string"
description: "Arranca mitmdump en modo headless en segundo plano como proxy de interceptación liviano, con rotación de capturas cada N minutos vía el addon rotate_capture_flows.py del registry. El proceso sobrevive al cierre de la shell (setsid). Emite JSON con PID, puerto, directorio de salida y ruta del log."
tags: [bash, cybersecurity, mitmproxy, proxy, capture, web-proxy, network, interception, mitmdump]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--port N"
desc: "Puerto TCP donde mitmdump escucha conexiones del cliente proxy. Default: 8080."
- name: "--out DIR"
desc: "Directorio donde se guardan los archivos .mitm rotados. Se crea automáticamente si no existe. Default: $HOME/captures."
- name: "--rotate-min N"
desc: "Minutos de duración de cada archivo de captura antes de rotar. Default: 20."
- name: "--addon PATH"
desc: "Ruta al addon Python de rotación (rotate_capture_flows.py). Default: derivado desde FN_REGISTRY_ROOT o desde la ubicación del propio script (3 niveles arriba)."
- name: "--mitmdump BIN"
desc: "Ruta al binario mitmdump. Default: autodetectado con command -v mitmdump, luego $HOME/.local/bin/mitmdump. Si no se encuentra, falla con instrucción de instalación."
- name: "--log PATH"
desc: "Archivo de log del proceso mitmdump. Default: <out>/mitmdump.log."
output: "JSON en stdout: {\"pid\": <pid>, \"port\": <port>, \"out_dir\": \"<dir>\", \"rotate_min\": <n>, \"log\": \"<log>\"}. Exit 1 en error (binario no encontrado, addon ausente, proceso muerto al arrancar)."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/start_mitm_capture.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/start_mitm_capture.sh
# Arranque con defaults (puerto 8080, capturas en ~/captures, rotación cada 20 min)
start_mitm_capture --port 8080 --out /home/enmanuel/captures --rotate-min 20
# Salida esperada:
# {"pid": 123456, "port": 8080, "out_dir": "/home/enmanuel/captures", "rotate_min": 20, "log": "/home/enmanuel/captures/mitmdump.log"}
# Puerto alternativo y rotación más frecuente
start_mitm_capture --port 9090 --out /tmp/mitm_session --rotate-min 5
# Pasando addon y binario explícitos
start_mitm_capture \
--port 8080 \
--out /home/enmanuel/captures \
--addon /home/enmanuel/fn_registry/python/functions/cybersecurity/rotate_capture_flows.py \
--mitmdump /home/enmanuel/.local/bin/mitmdump
# Leer el PID para poder parar el proceso más adelante
info=$(start_mitm_capture --port 8080 --out /home/enmanuel/captures)
pid=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)['pid'])")
echo "Proxy corriendo con PID $pid"
# Para parar: kill "$pid"
```
## Cuando usarla
Cuando necesites un proxy de interceptación pasivo siempre activo en segundo plano que capture y rote el tráfico HTTP/HTTPS de forma continua sin supervisión manual. Úsala antes de lanzar pruebas de integración, sesiones de auditoría web o scraping supervisado donde quieras replay o análisis posterior de las capturas `.mitm`.
## Gotchas
- **HTTPS requiere instalar el CA de mitmproxy en el cliente.** El certificado raíz está en `~/.mitmproxy/mitmproxy-ca-cert.pem` tras el primer arranque. En navegadores: importar como CA de confianza. En curl/httpx: `--cacert ~/.mitmproxy/mitmproxy-ca-cert.pem`. En Chrome/Chromium headless: `--ignore-certificate-errors` (solo para pruebas).
- **El proceso queda en background.** Para pararlo: `kill <pid>` (el PID se devuelve en el JSON) o `port_kill 8080` si tienes la función del registry disponible.
- **El addon rotate_capture_flows.py debe existir.** La función lo busca en la raíz del registry derivada automáticamente; si el registry está en una ubicación no estándar, pasa `--addon` explícitamente o setea `FN_REGISTRY_ROOT`.
- **setsid desacopla el proceso de la shell.** Si cierras la terminal, mitmdump sigue corriendo. Necesitas matar el PID manualmente o via systemd/supervisor si quieres ciclo de vida gestionado.
- **El log crece sin límite.** El log de mitmdump no rota; solo rotan los archivos `.mitm` de capturas. Monitoriza el tamaño de `<out>/mitmdump.log` en sesiones largas.
- **Solo funciona con mitmdump >= 9.x** (API de addons `--set` con parámetros por nombre). Versiones anteriores usan sintaxis distinta.
## Notas
La derivación de la raíz del registry cuando `FN_REGISTRY_ROOT` no está seteado asciende 3 niveles desde `bash/functions/cybersecurity/` hasta la raíz usando `dirname "${BASH_SOURCE[0]}"`. Esto funciona siempre que el script se sourcee desde su ubicación original en el registry.
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# start_mitm_capture — Arranca mitmdump en segundo plano como proxy de interceptación
# con rotación de capturas cada N minutos vía addon Python del registry.
start_mitm_capture() {
local port=8080
local out_dir="$HOME/captures"
local rotate_min=20
local addon_path=""
local mitmdump_bin=""
local log_path=""
# Parseo de argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--port) port="$2"; shift 2 ;;
--out) out_dir="$2"; shift 2 ;;
--rotate-min) rotate_min="$2"; shift 2 ;;
--addon) addon_path="$2"; shift 2 ;;
--mitmdump) mitmdump_bin="$2"; shift 2 ;;
--log) log_path="$2"; shift 2 ;;
*)
echo "ERROR: argumento desconocido: $1" >&2
echo "Uso: start_mitm_capture [--port N] [--out DIR] [--rotate-min N] [--addon PATH] [--mitmdump BIN] [--log PATH]" >&2
return 1
;;
esac
done
# Derivar raíz del registry cuando FN_REGISTRY_ROOT no está seteado
local registry_root
if [[ -n "${FN_REGISTRY_ROOT:-}" ]]; then
registry_root="$FN_REGISTRY_ROOT"
else
# bash/functions/cybersecurity/ -> 3 niveles arriba = raíz del registry
registry_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
fi
# Default addon usando la raíz derivada
if [[ -z "$addon_path" ]]; then
addon_path="${registry_root}/python/functions/cybersecurity/rotate_capture_flows.py"
fi
# Default log path (depende de out_dir, se resuelve después de crear el dir)
# Se asigna más abajo si sigue vacío
# Crear directorio de capturas si no existe
if [[ ! -d "$out_dir" ]]; then
mkdir -p "$out_dir" || {
echo "ERROR: no se pudo crear el directorio de capturas: $out_dir" >&2
return 1
}
fi
# Asignar log por defecto ahora que out_dir existe
if [[ -z "$log_path" ]]; then
log_path="${out_dir}/mitmdump.log"
fi
# Resolver binario mitmdump
if [[ -z "$mitmdump_bin" ]]; then
if command -v mitmdump &>/dev/null; then
mitmdump_bin="$(command -v mitmdump)"
elif [[ -x "$HOME/.local/bin/mitmdump" ]]; then
mitmdump_bin="$HOME/.local/bin/mitmdump"
else
echo "ERROR: mitmdump no encontrado; instala con: uv tool install mitmproxy" >&2
return 1
fi
else
if [[ ! -x "$mitmdump_bin" ]]; then
echo "ERROR: el binario indicado no existe o no es ejecutable: $mitmdump_bin" >&2
return 1
fi
fi
# Verificar que el addon existe
if [[ ! -f "$addon_path" ]]; then
echo "ERROR: addon no encontrado: $addon_path" >&2
echo " Asegúrate de que FN_REGISTRY_ROOT apunta a la raíz del registry" >&2
echo " o pasa --addon con la ruta correcta a rotate_capture_flows.py" >&2
return 1
fi
# Arrancar mitmdump en background con setsid (sobrevive al cierre de la shell).
# El redirect </dev/null desacopla stdin: sin él, el proceso retiene el pipe
# heredado y la shell sale con exit 144 (SIGTERM) al cerrarse. disown lo saca
# de la tabla de jobs para que la shell no le envíe señales al terminar.
setsid "$mitmdump_bin" \
-s "$addon_path" \
--set "rotate_min=${rotate_min}" \
--set "capture_dir=${out_dir}" \
--listen-port "$port" \
</dev/null >> "$log_path" 2>&1 &
local pid=$!
disown "$pid" 2>/dev/null || true
# Esperar ~1s y verificar que el proceso sigue vivo
sleep 1
if ! kill -0 "$pid" 2>/dev/null; then
echo "ERROR: mitmdump murió al arrancar. Últimas líneas del log:" >&2
tail -20 "$log_path" >&2
return 1
fi
# Emitir JSON con información del proceso arrancado
printf '{"pid": %d, "port": %d, "out_dir": "%s", "rotate_min": %d, "log": "%s"}\n' \
"$pid" "$port" "$out_dir" "$rotate_min" "$log_path"
}
# Ejecutar si se llama directamente (fn run / bash <file>)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
start_mitm_capture "$@"
fi
@@ -22,14 +22,14 @@ params:
- name: app_name
desc: "Nombre de la app (ej: chart_demo). Se usa para localizar cpp/build/windows/apps/<app>/<app>.exe y el directorio destino Desktop/apps/<app>/."
- name: app_dir
desc: "Ruta absoluta al directorio fuente de la app (ej: /home/lucas/fn_registry/cpp/apps/chart_demo). Se usa para localizar enrichers/, runtime/ y app.md."
desc: "Ruta absoluta al directorio fuente de la app (ej: $HOME/fn_registry/cpp/apps/chart_demo). Se usa para localizar enrichers/, runtime/ y app.md."
output: "Copia archivos al escritorio de Windows. Imprime 'OK: <app> -> <dest>' en stdout. Si local_files/ existe, imprime su tamanio. Errores fatales a stderr con exit 1."
---
## Ejemplo
```bash
deploy_cpp_exe_to_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_demo"
deploy_cpp_exe_to_windows "chart_demo" "$HOME/fn_registry/cpp/apps/chart_demo"
# OK: chart_demo -> /mnt/c/Users/lucas/Desktop/apps/chart_demo
# Con rutas custom via env vars
@@ -55,7 +55,7 @@ Desktop/apps/<APP>/
- `BUILD_WIN` — directorio de build Windows; default `$FN_REGISTRY_ROOT/cpp/build/windows`
- `WIN_DESKTOP_APPS` — directorio destino; default `/mnt/c/Users/lucas/Desktop/apps`
- `FN_REGISTRY_ROOT` — raiz del registry; default `/home/lucas/fn_registry`
- `FN_REGISTRY_ROOT` — raiz del registry; default `$HOME/fn_registry`
## Notas
@@ -12,7 +12,7 @@ deploy_cpp_exe_to_windows() {
return 1
fi
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
local root="${FN_REGISTRY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
@@ -0,0 +1,61 @@
---
name: deploy_wails_exe_to_windows
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "deploy_wails_exe_to_windows <app_name> <app_dir>"
description: "Copia el .exe de una app Wails desde <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso anterior (taskkill /F) y relanza la app via cmd.exe. Single-binary: no copia DLLs (Webview2 nativo en SO). Preserva local_files/ si existe."
tags: ["wails", "windows", "deploy", "cross-compile", "mingw", "infra", "launch", "matrix-mas"]
params:
- name: app_name
desc: "Nombre del binario sin extension (ej. matrix_client_pc). Debe coincidir con el nombre del .exe generado por wails build."
- name: app_dir
desc: "Ruta absoluta al directorio raiz de la app, donde vive build/bin/<app>.exe. Puede estar en projects/<project>/apps/<app>/ o apps/<app>/."
output: "Imprime pasos en stderr. En stdout: ls -lh del .exe desplegado. Exit 0 si ok, exit 1 si build/bin/<app>.exe no existe o los args estan vacios."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
- "args vacios devuelven error con mensaje de uso"
- "app_dir inexistente devuelve exit 1"
- "build/bin exe inexistente devuelve exit 1"
test_file_path: "bash/functions/infra/deploy_wails_exe_to_windows_test.sh"
file_path: "bash/functions/infra/deploy_wails_exe_to_windows.sh"
---
## Ejemplo
```bash
source bash/functions/infra/deploy_wails_exe_to_windows.sh
# Desplegar matrix_client_pc tras wails build -platform windows/amd64
deploy_wails_exe_to_windows matrix_client_pc \
$HOME/fn_registry/projects/element_agents/apps/matrix_client_pc
```
Con override de destino:
```bash
WIN_DESKTOP_APPS=/mnt/c/Users/lucas/Desktop/apps \
deploy_wails_exe_to_windows matrix_admin_panel \
$HOME/fn_registry/projects/element_agents/apps/matrix_admin_panel
```
## Cuando usarla
Tras un `wails build -platform windows/amd64` exitoso, para desplegar el binario compilado en Windows y relanzarlo en el mismo paso. Ideal en el ciclo de iteracion rapida: compilar → desplegar → ver cambios. Equivalente a `deploy_cpp_exe_to_windows_bash_infra` pero para apps Wails (single-binary sin DLLs extras).
## Gotchas
- **taskkill /F fuerza muerte** sin permitir guardado en disco. Las apps Wails persisten estado en keyring de Windows y AppData — este kill es seguro para ellas. Si la app tuviera autosave en progreso, se perderia (aceptable en ciclos de dev).
- **UNC paths prohibidos en cmd.exe**: `cmd.exe /c start` debe ejecutarse con `cd` previo al directorio Windows (`/mnt/c/...`). Intentar lanzar con path `\\wsl.localhost\...` falla con "UNC paths are not supported as the current directory".
- **cmd.exe start no bloquea**: la funcion espera 3s y verifica via `tasklist.exe`. Si la app cierra sola tras el arranque (error de inicio), el warn final lo indica pero no causa exit 1. Revisar logs en `%APPDATA%\<app>\` o `%LOCALAPPDATA%\<app>\`.
- **Single-binary Wails**: no copiar DLLs. Webview2 es nativo del SO (Windows 10+ ya lo incluye). Si una version vieja de Windows no tuviera Webview2, la app falla al arrancar — solucion: instalar Webview2 Runtime en esa maquina.
- **Build previo es responsabilidad del caller**: esta funcion NO compila. Para matrix_client_pc usa `-tags goolm` por el crypto de Matrix: `wails build -platform windows/amd64 -tags goolm`.
- **WIN_DESKTOP_APPS override**: variable de entorno para cambiar el destino. Util en CI o maquinas con escritorio en otra ruta.
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# deploy_wails_exe_to_windows — Copia el .exe de una app Wails compilado en
# <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso
# anterior y relanza la app. Single-binary: no copia DLLs (Webview2 nativo SO).
# Pre-authorized: taskkill.exe /F — idempotente, sin prompt.
set -euo pipefail
deploy_wails_exe_to_windows() {
local app="${1:-}"
local app_dir="${2:-}"
if [ -z "$app" ] || [ -z "$app_dir" ]; then
echo "ERROR: uso: deploy_wails_exe_to_windows <app_name> <app_dir>" >&2
echo " app_name: nombre del binario sin extension (ej. matrix_client_pc)" >&2
echo " app_dir: ruta absoluta al directorio de la app (donde vive build/bin/)" >&2
return 1
fi
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
# --- 1. Validar que el .exe existe ---
local exe_src="${app_dir}/build/bin/${app}.exe"
if [ ! -f "$exe_src" ]; then
echo "ERROR: no se encontro $exe_src" >&2
echo "Compila primero con: wails build -platform windows/amd64" >&2
return 1
fi
# --- 2. Crear directorio destino (preserva local_files/ si existe) ---
local dest="${win_desktop_apps}/${app}"
mkdir -p "$dest"
echo "[deploy_wails] dest: $dest" >&2
# --- 3. Matar proceso si esta corriendo en Windows ---
# Pre-authorized. Wails apps usan AppData+keyring para estado, kill /F es seguro.
if command -v taskkill.exe >/dev/null 2>&1; then
echo "[deploy_wails] matando ${app}.exe si corre..." >&2
taskkill.exe /IM "${app}.exe" /F 2>/dev/null || true
fi
# --- 4. Esperar a que Windows libere el file handle ---
sleep 1
# --- 5. Copiar .exe (cp -f: overwrite sin borrar el directorio) ---
echo "[deploy_wails] copiando ${app}.exe..." >&2
cp -f "$exe_src" "$dest/${app}.exe"
# --- 6. Copiar appicon.ico si existe (opcional, algunos hubs lo leen) ---
local icon_src="${app_dir}/appicon.ico"
if [ -f "$icon_src" ]; then
echo "[deploy_wails] copiando appicon.ico..." >&2
cp -f "$icon_src" "$dest/appicon.ico"
fi
# --- 7. Relanzar la app desde su dir Windows ---
# Usar cmd.exe /c start desde el dir destino (no UNC paths — falla en cmd.exe).
echo "[deploy_wails] lanzando ${app}.exe..." >&2
(
cd "$dest"
cmd.exe /c start "" "${app}.exe"
)
# --- 8. Dar tiempo a que el proceso arranque ---
sleep 3
# --- 9. Verificar que el proceso esta corriendo ---
if command -v tasklist.exe >/dev/null 2>&1; then
local tasklist_out
tasklist_out=$(tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null || true)
if echo "$tasklist_out" | grep -qi "^${app}.exe"; then
local pid
pid=$(echo "$tasklist_out" | grep -i "^${app}.exe" | awk '{print $2}' | head -n1)
echo "[deploy_wails] ${app}.exe corriendo con PID $pid" >&2
else
echo "WARN: ${app}.exe no aparece en tasklist tras el lanzamiento." >&2
echo " Puede que la app cerro con error. Revisar AppData para logs." >&2
fi
fi
# --- 10. Resumen final en stdout ---
ls -lh "$dest/${app}.exe"
echo "[deploy_wails] OK: ${app} deployado en $dest" >&2
if [ -d "$dest/local_files" ]; then
echo "[deploy_wails] local_files/ preservado: $(du -sh "$dest/local_files" | cut -f1)" >&2
fi
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
deploy_wails_exe_to_windows "$@"
fi
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Tests para deploy_wails_exe_to_windows
# Solo prueba validacion de argumentos y rutas — no ejecuta taskkill/cmd.exe reales.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/deploy_wails_exe_to_windows.sh"
PASS=0
FAIL=0
assert_eq() {
local test_name="$1" expected="$2" got="$3"
if [[ "$expected" == "$got" ]]; then
echo "PASS: $test_name"
PASS=$((PASS + 1))
else
echo "FAIL: $test_name — expected '$expected', got '$got'"
FAIL=$((FAIL + 1))
fi
}
# --- Test 1: args vacios devuelven error con mensaje de uso ---
actual_exit=0
deploy_wails_exe_to_windows >/dev/null 2>&1 || actual_exit=$?
assert_eq "args vacios devuelven error con mensaje de uso" "1" "$actual_exit"
# --- Test 2: app_dir inexistente devuelve exit 1 ---
actual_exit=0
deploy_wails_exe_to_windows "myapp" "/tmp/nonexistent_dir_$(date +%s)" >/dev/null 2>&1 || actual_exit=$?
assert_eq "app_dir inexistente devuelve exit 1" "1" "$actual_exit"
# --- Test 3: build/bin exe inexistente devuelve exit 1 ---
TMPDIR_APP=$(mktemp -d)
# Crear estructura de dir de app pero SIN el exe
mkdir -p "$TMPDIR_APP/build/bin"
actual_exit=0
deploy_wails_exe_to_windows "myapp" "$TMPDIR_APP" >/dev/null 2>&1 || actual_exit=$?
rm -rf "$TMPDIR_APP"
assert_eq "build/bin exe inexistente devuelve exit 1" "1" "$actual_exit"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
+3 -3
View File
@@ -30,15 +30,15 @@ file_path: "bash/functions/infra/discover_git_repos.sh"
source bash/functions/infra/discover_git_repos.sh
# Listar todos los repos bajo fn_registry
discover_git_repos /home/lucas/fn_registry
discover_git_repos $HOME/fn_registry
# Contar repos
discover_git_repos /home/lucas/fn_registry | wc -l
discover_git_repos $HOME/fn_registry | wc -l
# Iterar
while IFS= read -r repo; do
echo "Repo: $repo"
done < <(discover_git_repos /home/lucas/fn_registry)
done < <(discover_git_repos $HOME/fn_registry)
```
## Notas
+1 -1
View File
@@ -33,7 +33,7 @@ file_path: "bash/functions/infra/docker_cp_file.sh"
```bash
source functions/infra/docker_cp_file.sh
result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db)
result=$(docker_cp_file $HOME/fn_registry/registry.db metabase /registry.db)
echo "$result"
# {"local_size":524288,"remote_size":524288}
@@ -32,7 +32,7 @@ file_path: "bash/functions/infra/git_auto_commit_dirty.sh"
source bash/functions/infra/git_auto_commit_dirty.sh
# Commitear con mensaje automatico
subject=$(git_auto_commit_dirty /home/lucas/fn_registry)
subject=$(git_auto_commit_dirty $HOME/fn_registry)
echo "Commit: $subject"
# Commitear con mensaje fijo
@@ -17,7 +17,7 @@ error_type: "error_go_core"
imports: []
example: |
# Manual check
bash bash/functions/infra/git_hook_audit_app_drift.sh /home/lucas/fn_registry/apps/kanban
bash bash/functions/infra/git_hook_audit_app_drift.sh $HOME/fn_registry/apps/kanban
# Used by pre_commit_hook_install_bash_infra (v2 hook chain)
file_path: bash/functions/infra/git_hook_audit_app_drift.sh
+2 -2
View File
@@ -30,7 +30,7 @@ file_path: "bash/functions/infra/git_pull_with_stash.sh"
source bash/functions/infra/git_pull_with_stash.sh
# Pullear repo con auto-stash
status=$(git_pull_with_stash /home/lucas/fn_registry)
status=$(git_pull_with_stash $HOME/fn_registry)
echo "$status"
# [pulled] fn_registry
# o:
@@ -46,7 +46,7 @@ while IFS= read -r repo; do
if [[ "$result" == "[diverged]"* || "$result" == "[stash-conflict]"* ]]; then
diverged+=("$result")
fi
done < <(discover_git_repos /home/lucas/fn_registry)
done < <(discover_git_repos $HOME/fn_registry)
if [[ ${#diverged[@]} -gt 0 ]]; then
echo "ATENCION: repos que requieren intervencion manual:"
+2 -2
View File
@@ -30,7 +30,7 @@ file_path: "bash/functions/infra/git_push_if_ahead.sh"
source bash/functions/infra/git_push_if_ahead.sh
# Pushear si hay commits locales
status=$(git_push_if_ahead /home/lucas/fn_registry)
status=$(git_push_if_ahead $HOME/fn_registry)
echo "$status"
# [push] fn_registry (master, 3 commits ahead)
# o:
@@ -39,7 +39,7 @@ echo "$status"
# Iterar sobre multiples repos
while IFS= read -r repo; do
git_push_if_ahead "$repo"
done < <(discover_git_repos /home/lucas/fn_registry)
done < <(discover_git_repos $HOME/fn_registry)
```
## Estados de salida
+8 -1
View File
@@ -51,7 +51,14 @@ git_push_if_ahead() {
echo "[push] $repo_name ($branch, $ahead commits ahead)" >&2
local push_out
push_out=$(git -C "$abs_repo" push origin "$branch" 2>&1) || {
echo "[error] $repo_name: $(echo "$push_out" | tail -1)"
# Preservar las lineas con los keywords que el orquestador usa para
# decidir el auto-recover (rejected / fast-forward / non-fast-forward).
# Un `tail -1` plano se quedaba con la linea final de `hint:` y perdia
# "[rejected]" + "Updates were rejected", impidiendo el recover.
local reason
reason=$(echo "$push_out" | grep -iE 'rejected|fast-forward|denied|permission|error:' | head -3 | tr '\n' ' ')
[[ -z "$reason" ]] && reason=$(echo "$push_out" | tail -1)
echo "[error] $repo_name: $reason"
return 0
}
echo "$push_out" | tail -3 >&2
+76
View File
@@ -0,0 +1,76 @@
---
name: jupyter_mcp_serve
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
error_type: "error_go_core"
signature: "jupyter_mcp_serve.sh [--dry-run]"
description: "Arranca (o reusa) un Jupyter Lab colaborativo en un puerto propio y lanza el Jupyter MCP server enganchado por stdio. Entrypoint robusto para la entrada 'jupyter' de .mcp.json: garantiza que el MCP SIEMPRE tiene servidor al que conectarse, sin depender de que haya un jupyter en 8888."
tags: [notebook, jupyter, mcp, infra, launcher-glue]
uses_functions: []
uses_types: []
params:
- name: "--dry-run"
desc: "Opcional. Arranca/verifica jupyter pero NO hace exec del MCP; loguea el comando elegido. Para tests."
output: "Proceso jupyter-mcp-server enganchado por stdio a un Jupyter Lab colaborativo local (127.0.0.1, puerto JUPYTER_MCP_PORT, default 8899). Logs en ~/.fn_jupyter_mcp/. stdout reservado al protocolo MCP."
---
## Que hace
El MCP de Jupyter (datalayer `jupyter-mcp-server`) **no arranca jupyter**, solo se
conecta a uno existente. Si la URL configurada no tiene jupyter detras, el MCP
nunca conecta. En esta maquina `localhost:8888` es el **proxy HTTP del contenedor
VPN gluetun**, no un jupyter — por eso el MCP fallaba siempre.
Este wrapper resuelve la cadena entera:
1. Localiza el venv (`python/.venv`) y los binarios `jupyter` + `jupyter-mcp-server`.
2. Si ya hay un jupyter gestionado vivo en `127.0.0.1:$PORT` (`/api/status` = 200) lo reusa.
3. Si no, arranca `jupyter lab` colaborativo detached (RTC via `jupyter-collaboration`),
en `JUPYTER_MCP_ROOT` (default = raiz del repo, asi cualquier notebook del arbol es lanzable).
4. Detecta el dialecto de CLI del MCP (`--document-url` nuevo / `--jupyter-url` viejo / env vars)
y hace `exec` del MCP por `--transport stdio`.
Self-adapting: funciona aunque cambie la version de `jupyter-mcp-server`.
## Ejemplo
```bash
# Como lo usa Claude Code (entrada en .mcp.json):
# "jupyter": { "command": "bash", "args": ["bash/functions/infra/jupyter_mcp_serve.sh"] }
# Test manual (arranca jupyter en 8899, no lanza el MCP):
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
curl -s http://127.0.0.1:8899/api/status # {"started":..., "version":...}
# Cambiar puerto / raiz de notebooks:
JUPYTER_MCP_PORT=8900 JUPYTER_MCP_ROOT=/home/enmanuel/fn_registry/analysis \
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
```
## Cuando usarla
Cuando quieras que el MCP de Jupyter de Claude Code **siempre** tenga servidor:
es el `command` de la entrada `jupyter` en `.mcp.json`. No la invoques a mano salvo
para depurar (`--dry-run`) o para levantar el jupyter colaborativo sin el MCP.
## Gotchas
- **stdout reservado**: el MCP habla por stdout (protocolo stdio). El wrapper jamas
escribe a stdout — todo log va a stderr y `~/.fn_jupyter_mcp/wrapper.log`. No metas
`echo` a stdout aqui o rompes el handshake del MCP.
- **Puerto 8888 ocupado por gluetun** en esta maquina. Por eso el default es **8899**.
Si 8899 tambien se ocupa, exporta `JUPYTER_MCP_PORT`.
- **Token vacio**: solo escucha en `127.0.0.1` con `disable_check_xsrf` + `allow_origin '*'`.
Aceptable en local; NO exponer el puerto a la red.
- **venv requerido**: necesita `python/.venv` con `jupyterlab`, `jupyter-collaboration`
y `jupyter-mcp-server`. Reconstruir: `cd python && uv sync --extra jupyter`.
- El jupyter arrancado queda **detached** (nohup): persiste entre invocaciones del MCP.
Para pararlo: `python/.venv/bin/jupyter server stop 8899` o `pkill -f 'jupyter-lab.*8899'`.
## Capability growth log
v1.0.0 (2026-06-01) — version inicial. Wrapper auto-start: reusa/levanta jupyter
colaborativo en puerto propio (8899) y autodetecta el dialecto de CLI del MCP.
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# jupyter_mcp_serve — arranca (o reusa) un Jupyter Lab colaborativo y lanza el
# Jupyter MCP server enganchado a el por stdio. Pensado para ser el `command` de
# la entrada "jupyter" en .mcp.json: garantiza que el MCP SIEMPRE tiene servidor.
#
# Por que existe: el MCP datalayer NO arranca jupyter, solo se conecta. Si la URL
# apunta a un puerto sin jupyter (en esta maquina 8888 = proxy VPN gluetun), el
# MCP nunca conecta. Este wrapper levanta su propio jupyter en un puerto propio.
#
# Env overrides:
# JUPYTER_MCP_ROOT raiz de notebooks (default: raiz del repo)
# JUPYTER_MCP_PORT puerto del jupyter gestionado (default: 8899)
# JUPYTER_MCP_VENV venv (default: <repo>/python/.venv)
# JUPYTER_MCP_TOKEN token (default: "" — solo escucha en 127.0.0.1)
#
# stdout esta RESERVADO al protocolo stdio del MCP. Todo log va a stderr + LOGFILE.
# Nunca hacer echo a stdout aqui.
#
# Uso directo / test:
# bash jupyter_mcp_serve.sh --dry-run # arranca jupyter, NO exec del MCP, loguea args
set -euo pipefail
DRY=0
[ "${1:-}" = "--dry-run" ] && DRY=1
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# raiz del repo = tres niveles arriba de bash/functions/infra/
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
VENV="${JUPYTER_MCP_VENV:-$REPO_ROOT/python/.venv}"
ROOT_DIR="${JUPYTER_MCP_ROOT:-$REPO_ROOT}"
PORT="${JUPYTER_MCP_PORT:-8899}"
HOST=127.0.0.1
TOKEN="${JUPYTER_MCP_TOKEN:-}"
LOGDIR="${HOME}/.fn_jupyter_mcp"
mkdir -p "$LOGDIR"
LOGFILE="$LOGDIR/wrapper.log"
JLOG="$LOGDIR/jupyterlab.log"
log(){ printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOGFILE"; printf '%s\n' "$*" >&2; }
JUPYTER="$VENV/bin/jupyter"
MCP="$VENV/bin/jupyter-mcp-server"
if [ ! -x "$JUPYTER" ]; then
log "FATAL: $JUPYTER no existe. Instala: cd $REPO_ROOT/python && uv pip install --python .venv/bin/python3 jupyterlab jupyter-collaboration jupyter-mcp-server"
exit 1
fi
if [ ! -x "$MCP" ]; then
log "FATAL: $MCP no existe. Instala jupyter-mcp-server en el venv."
exit 1
fi
server_up(){
local code
code="$(curl -s -m 3 -o /dev/null -w '%{http_code}' "http://$HOST:$PORT/api/status?token=$TOKEN" 2>/dev/null || true)"
[ "$code" = "200" ]
}
if server_up; then
log "reuso jupyter existente en $HOST:$PORT"
else
log "arranco jupyter colaborativo en $HOST:$PORT (root=$ROOT_DIR)"
nohup "$JUPYTER" lab \
--no-browser \
--ServerApp.ip="$HOST" \
--ServerApp.port="$PORT" \
--ServerApp.root_dir="$ROOT_DIR" \
--IdentityProvider.token="$TOKEN" \
--ServerApp.disable_check_xsrf=True \
--ServerApp.allow_origin='*' \
>>"$JLOG" 2>&1 &
disown 2>/dev/null || true
# esperar hasta ~30s a que levante
for _ in $(seq 1 60); do
server_up && break
sleep 0.5
done
if ! server_up; then
log "FATAL: jupyter no levanto en 30s. Ver $JLOG"
exit 1
fi
log "jupyter arriba"
fi
BASE="http://$HOST:$PORT"
# Detectar el dialecto de CLI del MCP (cambia entre versiones de jupyter-mcp-server)
HELP="$("$MCP" --help 2>&1 || true)"
ARGS=(--transport stdio)
if printf '%s' "$HELP" | grep -q -- '--document-url'; then
ARGS+=(--document-url "$BASE" --runtime-url "$BASE")
printf '%s' "$HELP" | grep -q -- '--document-token' && ARGS+=(--document-token "$TOKEN" --runtime-token "$TOKEN")
elif printf '%s' "$HELP" | grep -q -- '--jupyter-url'; then
ARGS+=(--jupyter-url "$BASE" --jupyter-token "$TOKEN")
else
# fallback: variables de entorno que las distintas versiones reconocen
export DOCUMENT_URL="$BASE" RUNTIME_URL="$BASE" DOCUMENT_TOKEN="$TOKEN" RUNTIME_TOKEN="$TOKEN"
export JUPYTER_URL="$BASE" JUPYTER_TOKEN="$TOKEN"
fi
log "MCP cmd: $MCP ${ARGS[*]}"
if [ "$DRY" = "1" ]; then
log "--dry-run: no ejecuto el MCP. Jupyter sigue corriendo en $BASE"
exit 0
fi
exec "$MCP" "${ARGS[@]}"
@@ -61,7 +61,7 @@ Mitad complementaria de `deploy_cpp_exe_to_windows_bash_infra`. El flujo complet
build_cpp_windows "registry_dashboard"
# 2. Copiar al escritorio (mata proceso si corre, copia DLLs+assets)
deploy_cpp_exe_to_windows "registry_dashboard" "/home/lucas/fn_registry/apps/registry_dashboard"
deploy_cpp_exe_to_windows "registry_dashboard" "$HOME/fn_registry/apps/registry_dashboard"
# 3. Lanzar
launch_cpp_app_windows "registry_dashboard"
@@ -0,0 +1,89 @@
---
name: mas_client_register
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "mas_client_register(ssh_host: string, container: string, config_file: string, dry_run: bool) -> json"
description: "Registra y sincroniza clientes OAuth en Matrix Authentication Service (MAS) ejecutando mas-cli config sync dentro del container Docker remoto via SSH. Verifica sintaxis YAML, soporte dry-run para ver diff antes de aplicar, y emite JSON estructurado con resultado. Idempotente: re-ejecucion con misma config no genera cambios."
tags: [matrix, mas, oauth, oidc, migration, mas-migration, infra, docker, ssh, matrix-mas]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: ssh_host
desc: "alias SSH del VPS donde corre MAS (ej. organic-machine.com). Debe estar en ~/.ssh/config con key auth."
- name: container
desc: "nombre del container Docker con MAS (ej. element_matrix_chat-mas-1). El config dentro del container se espera en /data/config.yaml."
- name: config_file
desc: "ruta absoluta en el VPS al archivo mas/config.yaml (ej. /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml). MAS lo monta como /data/config.yaml."
- name: dry_run
desc: "flag opcional --dry-run: ejecuta mas-cli config dump y devuelve el estado sin aplicar cambios. Util para verificar antes de activar MSC3861."
output: "JSON con: status ('ok'|'dry-run'|'error'), applied (bool), clients_total (int), clients_diff (array de lineas del output de mas-cli), stderr (string con logs de error si aplica)."
tested: true
tests:
- "help flag emite JSON parseable"
- "args faltantes retornan JSON de error sin ssh"
- "jq disponible en host local"
test_file_path: "bash/functions/infra/mas_client_register_test.sh"
file_path: "bash/functions/infra/mas_client_register.sh"
---
## Ejemplo
```bash
# Dry-run: verificar que clients se aplicarian correctamente
source bash/functions/infra/mas_client_register.sh
mas_client_register \
--ssh-host organic-machine.com \
--container element_matrix_chat-mas-1 \
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml \
--dry-run
# Aplicar sync real (con --prune para eliminar clients viejos)
mas_client_register \
--ssh-host organic-machine.com \
--container element_matrix_chat-mas-1 \
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml
```
Salida esperada (sync OK):
```json
{
"status": "ok",
"applied": true,
"clients_total": 6,
"clients_diff": ["synced client element-web", "synced client synapse-admin", "..."],
"stderr": ""
}
```
Salida dry-run:
```json
{
"status": "dry-run",
"applied": false,
"clients_total": 42,
"clients_diff": ["clients:", " - client_id: element-web", " ..."],
"stderr": ""
}
```
## Cuando usarla
Usar despues de editar `mas/config.yaml` localmente y antes de hacer restart a Synapse con `msc3861` habilitado en `homeserver.yaml`. Ejecutar primero con `--dry-run` para verificar que los 6 clients OAuth (Element Web, Synapse-Admin, matrix_client_pc, matrix_client_android, matrix_admin_panel, Synapse-internal) estan correctamente definidos, luego sin `--dry-run` para aplicar el sync.
## Gotchas
- **`--prune` elimina clients no declarados en config**: el sync real usa `--prune`, lo que borra cualquier client OAuth que exista en MAS pero no este en el `config.yaml`. Verificar con `--dry-run` antes de aplicar en produccion.
- **Requiere `jq` en el host local**: el JSON output se construye con `jq`. Si no esta instalado, la funcion falla con error claro antes de conectar al VPS.
- **`mas-cli` debe estar en el container**: la funcion asume que `mas-cli` esta en el PATH dentro del container MAS. Si el container usa una imagen diferente, verificar con `docker exec <container> mas-cli --version`.
- **Config dentro del container siempre en `/data/config.yaml`**: el `--config-file` apunta a la ruta en el VPS (para que el operador sepa que archivo editar), pero el comando dentro del container usa `/data/config.yaml` (el mount point estandar de MAS). Si el compose monta el archivo en otro path, ajustar la constante `container_config` en el script.
- **SSH key debe estar en agent o `~/.ssh/config`**: la funcion usa `ssh <alias>` directamente. Si la key requiere passphrase, ejecutar `ssh-add` antes.
- **Si `config.yaml` es invalido, sync aborta sin tocar estado**: el paso 1 (`mas-cli config check`) detecta errores de sintaxis YAML antes de intentar sync. El estado de MAS no se modifica si la config tiene errores.
- **Idempotente**: re-ejecutar con la misma config no genera cambios en MAS (mas-cli detecta que el estado ya coincide).
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env bash
# mas_client_register — Registra/sincroniza clientes OAuth en Matrix Authentication Service (MAS)
# via mas-cli config sync ejecutado en container Docker remoto a traves de SSH.
set -euo pipefail
mas_client_register() {
local ssh_host=""
local container=""
local config_file=""
local dry_run=false
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--ssh-host)
ssh_host="$2"
shift 2
;;
--container)
container="$2"
shift 2
;;
--config-file)
config_file="$2"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
--help|-h)
cat >&2 <<'USAGE'
mas_client_register - Sincroniza clientes OAuth en MAS via mas-cli config sync
Usage:
mas_client_register --ssh-host <host> --container <name> --config-file <path> [--dry-run]
Options:
--ssh-host Alias SSH del VPS (ej. organic-machine.com)
--container Nombre del container MAS (ej. element_matrix_chat-mas-1)
--config-file Ruta en el VPS al mas/config.yaml (ej. /home/ubuntu/project/mas/config.yaml)
--dry-run Solo valida config y muestra diff, sin aplicar cambios
Output: JSON en stdout con status, applied, clients_total, clients_diff, stderr
USAGE
# emit minimal valid JSON so callers that parse stdout don't break
echo '{"status":"help","applied":false,"clients_total":0,"clients_diff":[],"stderr":""}'
return 0
;;
*)
echo "mas_client_register: argumento desconocido: $1" >&2
return 1
;;
esac
done
# Validar argumentos obligatorios
local errors=()
[[ -z "$ssh_host" ]] && errors+=("--ssh-host es obligatorio")
[[ -z "$container" ]] && errors+=("--container es obligatorio")
[[ -z "$config_file" ]] && errors+=("--config-file es obligatorio")
if [[ ${#errors[@]} -gt 0 ]]; then
for err in "${errors[@]}"; do
echo "ERROR: $err" >&2
done
echo '{"status":"error","applied":false,"clients_total":0,"clients_diff":[],"stderr":"missing required arguments"}'
return 1
fi
# Verificar dependencias locales
if ! command -v jq &>/dev/null; then
echo "ERROR: jq no encontrado en el host local. Instalar: apt install jq / brew install jq" >&2
echo '{"status":"error","applied":false,"clients_total":0,"clients_diff":[],"stderr":"jq not found on local host"}'
return 1
fi
echo "mas_client_register: ssh-host=$ssh_host container=$container dry-run=$dry_run" >&2
# La ruta de config dentro del container siempre es /data/config.yaml (mount convention de MAS)
local container_config="/data/config.yaml"
# ---- PASO 1: Verificar sintaxis YAML con mas-cli config check ----
echo "mas_client_register: verificando sintaxis de config con mas-cli config check..." >&2
local check_stdout check_stderr check_exit
check_stdout=$(ssh "$ssh_host" \
"docker exec ${container} mas-cli config check --config ${container_config}" 2>/tmp/mas_check_stderr_$$ || true)
check_exit=$?
check_stderr=$(cat /tmp/mas_check_stderr_$$ 2>/dev/null || true)
rm -f /tmp/mas_check_stderr_$$
if [[ $check_exit -ne 0 ]]; then
echo "mas_client_register: config check falló (exit=$check_exit)" >&2
echo "$check_stderr" >&2
local escaped_stderr
escaped_stderr=$(printf '%s' "${check_stderr}" | jq -Rs '.')
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
return 1
fi
echo "mas_client_register: config check OK" >&2
# ---- PASO 2: dry-run o sync ----
if [[ "$dry_run" == "true" ]]; then
# Ejecutar mas-cli config dump para mostrar el estado actual y lo que se aplicaria
echo "mas_client_register: modo dry-run — ejecutando mas-cli config dump..." >&2
local dump_stdout dump_stderr dump_exit
dump_stdout=$(ssh "$ssh_host" \
"docker exec ${container} mas-cli config dump --config ${container_config}" 2>/tmp/mas_dump_stderr_$$ || true)
dump_exit=$?
dump_stderr=$(cat /tmp/mas_dump_stderr_$$ 2>/dev/null || true)
rm -f /tmp/mas_dump_stderr_$$
if [[ $dump_exit -ne 0 ]]; then
echo "mas_client_register: config dump falló (exit=$dump_exit)" >&2
echo "$dump_stderr" >&2
local escaped_stderr
escaped_stderr=$(printf '%s' "${dump_stderr}" | jq -Rs '.')
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
return 1
fi
# Extraer listado de clients del dump (buscar lineas con client_id o type: client)
local clients_diff_raw
clients_diff_raw=$(printf '%s\n' "$dump_stdout" | grep -E "client_id:|client_name:" | \
sed 's/^[[:space:]]*//' | head -50 || true)
local diff_json
diff_json=$(printf '%s\n' "$dump_stdout" | jq -Rs 'split("\n") | map(select(length > 0)) | map(ltrimstr(" "))' 2>/dev/null \
|| echo '["(jq parse error — ver stderr)"]')
local escaped_dump_stderr
escaped_dump_stderr=$(printf '%s' "${dump_stderr}" | jq -Rs '.')
echo "mas_client_register: dry-run completado. dump lines=$(echo "$dump_stdout" | wc -l)" >&2
jq -n \
--argjson diff "$diff_json" \
--argjson stderr_str "$escaped_dump_stderr" \
'{
status: "dry-run",
applied: false,
clients_total: ($diff | length),
clients_diff: $diff,
stderr: $stderr_str
}'
return 0
fi
# ---- PASO 3: sync real ----
echo "mas_client_register: ejecutando mas-cli config sync --prune..." >&2
local sync_stdout sync_stderr sync_exit
sync_stdout=$(ssh "$ssh_host" \
"docker exec ${container} mas-cli config sync --config ${container_config} --prune" \
2>/tmp/mas_sync_stderr_$$ || true)
sync_exit=$?
sync_stderr=$(cat /tmp/mas_sync_stderr_$$ 2>/dev/null || true)
rm -f /tmp/mas_sync_stderr_$$
echo "mas_client_register: sync exit=$sync_exit" >&2
if [[ -n "$sync_stderr" ]]; then
echo "mas_client_register stderr: $sync_stderr" >&2
fi
if [[ $sync_exit -ne 0 ]]; then
local escaped_stderr
escaped_stderr=$(printf '%s' "${sync_stderr}" | jq -Rs '.')
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
return 1
fi
# Parsear output del sync para extraer lineas con cambios aplicados
local diff_lines
diff_lines=$(printf '%s\n' "$sync_stdout" | grep -E "^\s*(created|updated|deleted|unchanged|synced)" || true)
local diff_json
diff_json=$(printf '%s\n' "$sync_stdout" | jq -Rs 'split("\n") | map(select(length > 0))' 2>/dev/null \
|| echo '[]')
local clients_count
clients_count=$(printf '%s\n' "$sync_stdout" | grep -cE "client" 2>/dev/null || echo 0)
local escaped_sync_stderr
escaped_sync_stderr=$(printf '%s' "${sync_stderr}" | jq -Rs '.')
echo "mas_client_register: sync completado con exito" >&2
jq -n \
--argjson diff "$diff_json" \
--argjson total "$clients_count" \
--argjson stderr_str "$escaped_sync_stderr" \
'{
status: "ok",
applied: true,
clients_total: $total,
clients_diff: $diff,
stderr: $stderr_str
}'
}
# Ejecutar si se llama directamente (no sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
mas_client_register "$@"
fi
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Tests para mas_client_register
# No requiere SSH real — prueba paths locales (arg validation, --help, JSON output)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PASS=0
FAIL=0
assert_contains() {
local test_name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
((PASS++))
else
echo "FAIL: $test_name — expected to contain '$needle', got: $haystack"
((FAIL++))
fi
}
assert_json_parseable() {
local test_name="$1" json="$2"
if command -v jq &>/dev/null; then
if echo "$json" | jq . >/dev/null 2>&1; then
echo "PASS: $test_name"
((PASS++))
else
echo "FAIL: $test_name — output no es JSON valido: $json"
((FAIL++))
fi
else
if [[ "$json" == \{* ]]; then
echo "PASS: $test_name (jq no disponible, verificacion basica OK)"
((PASS++))
else
echo "FAIL: $test_name — output no parece JSON: $json"
((FAIL++))
fi
fi
}
# Test: help flag emite JSON parseable
# Cada invocacion en subshell aislada para no contaminar el runner con set -e del script fuente
bash "$SCRIPT_DIR/mas_client_register.sh" --help >/tmp/mas_test_help_$$ 2>/dev/null || true
output_help=$(cat /tmp/mas_test_help_$$ 2>/dev/null || true)
rm -f /tmp/mas_test_help_$$
assert_json_parseable "help flag emite JSON parseable" "$output_help"
# Test: args faltantes retornan JSON de error sin ssh
bash "$SCRIPT_DIR/mas_client_register.sh" >/tmp/mas_test_noargs_$$ 2>/dev/null || true
output_noargs=$(cat /tmp/mas_test_noargs_$$ 2>/dev/null || true)
rm -f /tmp/mas_test_noargs_$$
assert_json_parseable "args faltantes retornan JSON de error sin ssh" "$output_noargs"
assert_contains "args faltantes contienen status error" '"status":"error"' "$output_noargs"
# Test: jq disponible en host local
if command -v jq &>/dev/null; then
echo "PASS: jq disponible en host local"
((PASS++))
else
echo "FAIL: jq disponible en host local — instalar: apt install jq"
((FAIL++))
fi
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
@@ -0,0 +1,83 @@
---
name: mas_syn2mas_migration
kind: function
lang: bash
domain: infra
version: "0.1.0"
purity: impure
signature: "mas_syn2mas_migration --ssh-host <host> --mas-container <name> --synapse-config-path <path-on-host> --log-dir <local-path> [--max-conflicts N] [--apply]"
description: "Migra usuarios Synapse a Matrix Authentication Service (MAS) via mas-cli syn2mas. Fuerza dry-run primero, archiva el log, aborta si los conflicts superan el threshold, y solo ejecuta la migracion real con --apply."
tags: [matrix, mas, syn2mas, migration, mas-migration, infra, users, docker, ssh, matrix-mas]
params:
- name: ssh-host
desc: "Alias SSH del VPS donde corren los containers (ej. organic-machine.com)"
- name: mas-container
desc: "Nombre del container Docker de MAS (ej. element_matrix_chat-mas-1)"
- name: synapse-config-path
desc: "Ruta en el VPS al homeserver.yaml de Synapse (ej. /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml). El container debe tener el archivo accesible en /data/homeserver.yaml via volume mount."
- name: log-dir
desc: "Directorio local donde archivar logs dry-run y apply. Se crea con chmod 0700 y los logs con 0600 (contienen userIDs)."
- name: max-conflicts
desc: "Tope de conflictos detectados en dry-run. Si conflicts > max-conflicts, status=aborted exit 2. Default 0 (abortar ante cualquier conflict)."
- name: apply
desc: "Flag booleano. Sin --apply: solo dry-run (status=ok, sin cambios). Con --apply: ejecuta la migracion real tras pasar el threshold."
output: "JSON en stdout: {\"status\":\"ok|aborted|error\",\"dry_run_log\":\"path\",\"apply_log\":\"path|null\",\"conflicts\":N,\"users_migrated\":N,\"duration_s\":N}. Exit 0=ok, 1=error, 2=aborted por conflicts."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
- "aborta con error cuando faltan args obligatorios"
- "help no devuelve error"
- "argumento desconocido retorna exit 1"
- "max-conflicts invalido retorna exit 1"
test_file_path: "bash/functions/infra/mas_syn2mas_migration_test.sh"
file_path: "bash/functions/infra/mas_syn2mas_migration.sh"
---
## Ejemplo
```bash
# Paso 1: dry-run OBLIGATORIO (sin --apply — no modifica nada)
mas_syn2mas_migration \
--ssh-host organic-machine.com \
--mas-container element_matrix_chat-mas-1 \
--synapse-config-path /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml \
--log-dir ~/matrix_migration_logs \
--max-conflicts 0
# Salida esperada (si hay 0 conflicts):
# {"status":"ok","dry_run_log":"/home/lucas/matrix_migration_logs/syn2mas_dryrun_1234567890.log","apply_log":null,"conflicts":0,"users_migrated":0,"duration_s":0}
# Revisar el log antes de continuar:
# cat ~/matrix_migration_logs/syn2mas_dryrun_*.log
# Paso 2: tras revisar el log dry-run, aplicar la migracion real
mas_syn2mas_migration \
--ssh-host organic-machine.com \
--mas-container element_matrix_chat-mas-1 \
--synapse-config-path /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml \
--log-dir ~/matrix_migration_logs \
--max-conflicts 0 \
--apply
# Salida esperada tras migracion exitosa:
# {"status":"ok","dry_run_log":"/home/lucas/matrix_migration_logs/syn2mas_dryrun_1234567890.log","apply_log":"/home/lucas/matrix_migration_logs/syn2mas_apply_1234567890.log","conflicts":0,"users_migrated":42,"duration_s":15}
```
## Cuando usarla
Usar en el paso 4 de la migracion del issue 0162 (Synapse a MAS auth), tras activar MSC3861 en `homeserver.yaml` y verificar que MAS esta corriendo con `syn2mas: true` en su config. NUNCA ejecutar antes de activar MSC3861 — sin ese flag activo, `syn2mas` no puede mapear usuarios a las tablas MAS y la migracion resultara en estado inconsistente.
## Gotchas
- **Dry-run NO modifica nada** — siempre ejecutar primero sin `--apply` y revisar el log manualmente antes de aplicar.
- Si el dry-run detecta usuarios con **guest accounts**, **application services** (bots), o **passwords externos** (LDAP/OIDC), revisar manualmente el log antes de aplicar — estos casos pueden requerir steps adicionales documentados en el issue 0162.
- **Backup postgres pre-migracion NO esta cubierto** por esta funcion. El operador es responsable de hacer `pg_dump` de la DB de Synapse antes de ejecutar con `--apply`. Ver issue 0162 paso 1.
- Si la migracion real falla **a mitad**, MAS puede quedar en estado inconsistente con usuarios parcialmente migrados. El rollback consiste en restaurar el backup postgres de Synapse + revertir `homeserver.yaml` a la configuracion pre-MSC3861.
- Los logs archivados en `--log-dir` **incluyen userIDs** (datos personales). Se crean con permisos `0600` (solo propietario puede leer). Mantener el directorio con `chmod 0700`. No subir los logs a repos publicos.
- El comando `mas-cli syn2mas` en el container asume que `homeserver.yaml` esta montado en `/data/homeserver.yaml`. Si el volume mount del container usa otra ruta, el comando fallara con "file not found". Verificar con `docker inspect <container> | jq '.[].Mounts'`.
- La postcondicion compara el count de usuarios MAS con una segunda ejecucion de dry-run para obtener el count esperado. Si el conteo no esta disponible (salida inesperada de mas-cli), la funcion emite `status=ok` con `users_migrated` del count real de MAS — no aborta por este motivo para evitar falsos negativos.
@@ -0,0 +1,325 @@
#!/usr/bin/env bash
# mas_syn2mas_migration — Migra usuarios Synapse a MAS via mas-cli syn2mas.
# Fuerza dry-run primero, archiva el log, aborta si conflicts > threshold,
# y solo ejecuta la migracion real cuando se pasa --apply.
#
# Usage:
# mas_syn2mas_migration --ssh-host <host> --mas-container <name> \
# --synapse-config-path <path-on-host> --log-dir <local-path> \
# [--max-conflicts N] [--apply]
#
# Output: JSON en stdout con status, dry_run_log, apply_log, conflicts, users_migrated, duration_s
set -euo pipefail
mas_syn2mas_migration() {
local ssh_host=""
local mas_container=""
local synapse_config_path=""
local log_dir=""
local max_conflicts=0
local do_apply=false
# ---- Parse args ----
while [[ $# -gt 0 ]]; do
case "$1" in
--ssh-host)
ssh_host="$2"
shift 2
;;
--mas-container)
mas_container="$2"
shift 2
;;
--synapse-config-path)
synapse_config_path="$2"
shift 2
;;
--log-dir)
log_dir="$2"
shift 2
;;
--max-conflicts)
max_conflicts="$2"
shift 2
;;
--apply)
do_apply=true
shift
;;
--help|-h)
cat >&2 <<'USAGE'
mas_syn2mas_migration - Migra usuarios Synapse a Matrix Authentication Service (MAS)
Usage:
mas_syn2mas_migration \
--ssh-host <host> \
--mas-container <name> \
--synapse-config-path <path-on-host> \
--log-dir <local-path> \
[--max-conflicts N] \
[--apply]
Opciones:
--ssh-host Alias SSH del VPS (ej. organic-machine.com)
--mas-container Nombre del container MAS (ej. element_matrix_chat-mas-1)
--synapse-config-path Ruta en el VPS al homeserver.yaml
(ej. /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml)
--log-dir Directorio local donde archivar logs dry-run y apply
--max-conflicts N Tope de conflictos en dry-run antes de abortar (default 0)
--apply Ejecutar migracion real. Sin esta flag: solo dry-run.
Comportamiento:
1. Siempre ejecuta dry-run primero y archiva el log.
2. Si conflicts > max-conflicts -> status=aborted, exit 2.
3. Sin --apply -> status=ok (dry-run completado), exit 0.
4. Con --apply -> ejecuta migracion real, archiva log, verifica postcondicion.
Output JSON: {"status":"ok|aborted|error","dry_run_log":"path","apply_log":"path|null","conflicts":N,"users_migrated":N,"duration_s":N}
USAGE
echo '{"status":"help","dry_run_log":"","apply_log":null,"conflicts":0,"users_migrated":0,"duration_s":0}'
return 0
;;
*)
echo "mas_syn2mas_migration: argumento desconocido: $1" >&2
return 1
;;
esac
done
# ---- Validar argumentos obligatorios ----
local errors=()
[[ -z "$ssh_host" ]] && errors+=("--ssh-host es obligatorio")
[[ -z "$mas_container" ]] && errors+=("--mas-container es obligatorio")
[[ -z "$synapse_config_path" ]] && errors+=("--synapse-config-path es obligatorio")
[[ -z "$log_dir" ]] && errors+=("--log-dir es obligatorio")
if [[ ${#errors[@]} -gt 0 ]]; then
for err in "${errors[@]}"; do
echo "ERROR: $err" >&2
done
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
return 1
fi
# Validar que max_conflicts es un entero no negativo
if ! [[ "$max_conflicts" =~ ^[0-9]+$ ]]; then
echo "ERROR: --max-conflicts debe ser un entero >= 0, recibido: $max_conflicts" >&2
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
return 1
fi
# ---- Dependencias locales ----
if ! command -v jq &>/dev/null; then
echo "ERROR: jq no encontrado. Instalar: apt install jq / brew install jq" >&2
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
return 1
fi
# ---- Crear log-dir con permisos restringidos ----
mkdir -p "$log_dir"
chmod 0700 "$log_dir"
local ts
ts=$(date +%s)
local dry_run_log="${log_dir}/syn2mas_dryrun_${ts}.log"
local apply_log_path="null"
local apply_log_file="${log_dir}/syn2mas_apply_${ts}.log"
# La ruta del homeserver.yaml dentro del container MAS se pasa como --synapse-config
# MAS monta el directorio del synapse bajo /data/ por convencion, pero la ruta real
# puede variar — usamos la ruta tal como existe en el host (montada via volume).
# El comando real esperado: docker exec <container> mas-cli syn2mas --synapse-config <path>
# donde <path> es la ruta tal como el container la ve (via volume mount).
# Asumimos que el VPS tiene el config accesible en la misma ruta dentro del container.
local container_config="/data/homeserver.yaml"
echo "mas_syn2mas_migration: ssh-host=${ssh_host} container=${mas_container} max-conflicts=${max_conflicts} apply=${do_apply}" >&2
# =========================================================================
# PASO 1: DRY-RUN obligatorio
# =========================================================================
echo "mas_syn2mas_migration: ejecutando dry-run..." >&2
local dry_exit=0
# Capturar stdout+stderr del dry-run en el log y tambien en variable para parsing
local dry_output
dry_output=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli syn2mas \
--synapse-config '${container_config}' \
--dry-run" \
2>&1) || dry_exit=$?
# Archivar log con timestamp + header informativo
{
echo "# mas_syn2mas_migration dry-run"
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
echo "# synapse-config-path=${synapse_config_path}"
echo "# exit=${dry_exit}"
echo "# ---"
printf '%s\n' "$dry_output"
} > "$dry_run_log"
chmod 0600 "$dry_run_log"
echo "mas_syn2mas_migration: dry-run exit=${dry_exit}, log=${dry_run_log}" >&2
if [[ $dry_exit -ne 0 ]]; then
# Si el comando SSH falla completamente (no es fallo de syn2mas sino de conectividad)
echo "mas_syn2mas_migration: ERROR — dry-run falló con exit ${dry_exit}" >&2
local escaped_out
escaped_out=$(printf '%s' "${dry_output}" | jq -Rs '.')
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
return 1
fi
# =========================================================================
# PASO 2: Parsear conflicts del dry-run
# =========================================================================
# Regex sobre lineas tipo:
# "Conflict:", "Skipping:", "Error processing user", "conflict"
# También contamos líneas que indiquen usuarios problemáticos.
local conflicts=0
local conflict_lines
conflict_lines=$(printf '%s\n' "$dry_output" | \
grep -ciE '(conflict|skipping|error processing user|cannot migrate|already exists)' 2>/dev/null || true)
# grep -c devuelve string; convertir a int defensivamente
if [[ "$conflict_lines" =~ ^[0-9]+$ ]]; then
conflicts=$conflict_lines
else
# Parser falló de forma inesperada — abortar defensivamente
echo "mas_syn2mas_migration: ERROR — no se pudo parsear el conteo de conflicts del dry-run (parser defensivo)" >&2
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
return 1
fi
echo "mas_syn2mas_migration: conflicts detectados en dry-run: ${conflicts} (max permitido: ${max_conflicts})" >&2
# =========================================================================
# PASO 3: Verificar threshold de conflicts
# =========================================================================
if [[ $conflicts -gt $max_conflicts ]]; then
echo "mas_syn2mas_migration: ABORTADO — conflicts (${conflicts}) > max-conflicts (${max_conflicts})" >&2
echo "Revisar: ${dry_run_log}" >&2
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"aborted\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
return 2
fi
# =========================================================================
# PASO 4: Si no --apply, terminar aqui con status=ok (dry-run completado)
# =========================================================================
if [[ "$do_apply" == "false" ]]; then
echo "mas_syn2mas_migration: dry-run completado (${conflicts} conflicts). Revisar log y re-ejecutar con --apply." >&2
local dry_run_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
echo "{\"status\":\"ok\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
return 0
fi
# =========================================================================
# PASO 5: Migracion REAL (--apply)
# =========================================================================
echo "mas_syn2mas_migration: ejecutando migracion REAL..." >&2
local apply_start
apply_start=$(date +%s)
local apply_exit=0
local apply_output
apply_output=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli syn2mas \
--synapse-config '${container_config}'" \
2>&1) || apply_exit=$?
local apply_end
apply_end=$(date +%s)
local duration_s=$(( apply_end - apply_start ))
# Archivar log de apply
{
echo "# mas_syn2mas_migration apply"
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
echo "# synapse-config-path=${synapse_config_path}"
echo "# exit=${apply_exit} duration_s=${duration_s}"
echo "# ---"
printf '%s\n' "$apply_output"
} > "$apply_log_file"
chmod 0600 "$apply_log_file"
apply_log_path="$apply_log_file"
echo "mas_syn2mas_migration: apply exit=${apply_exit}, duration=${duration_s}s, log=${apply_log_file}" >&2
if [[ $apply_exit -ne 0 ]]; then
echo "mas_syn2mas_migration: ERROR — migracion real falló con exit ${apply_exit}" >&2
local dry_run_log_json apply_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":${duration_s}}"
return 1
fi
# =========================================================================
# PASO 6: Postcondicion — comparar usuarios en MAS vs Synapse
# =========================================================================
echo "mas_syn2mas_migration: verificando postcondicion (usuarios MAS vs Synapse)..." >&2
local mas_user_count=0
local synapse_user_count=0
local users_migrated=0
local post_status="ok"
# Contar usuarios en MAS via mas-cli admin user list
local mas_count_raw
mas_count_raw=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli manage list-users --json 2>/dev/null | jq length" \
2>/dev/null || echo "0")
if [[ "$mas_count_raw" =~ ^[0-9]+$ ]]; then
mas_user_count=$mas_count_raw
else
echo "mas_syn2mas_migration: ADVERTENCIA — no se pudo obtener conteo de usuarios MAS (output: ${mas_count_raw})" >&2
post_status="ok" # No abortar, solo advertir
fi
# Contar usuarios locales en Synapse via psql (excluyendo bots/AS)
# Intentamos obtener el count; si falla, continuamos sin abortar
local synapse_count_raw
synapse_count_raw=$(ssh "$ssh_host" \
"docker exec '${mas_container}' mas-cli syn2mas --synapse-config '${container_config}' --dry-run 2>&1 | grep -oE 'Found [0-9]+ users' | grep -oE '[0-9]+' | head -1" \
2>/dev/null || echo "0")
if [[ "$synapse_count_raw" =~ ^[0-9]+$ ]]; then
synapse_user_count=$synapse_count_raw
fi
users_migrated=$mas_user_count
# Si tenemos ambos counts y difieren significativamente, marcar como warning en log
if [[ $synapse_user_count -gt 0 && $mas_user_count -eq 0 ]]; then
echo "mas_syn2mas_migration: ADVERTENCIA — MAS reporta 0 usuarios pero Synapse tenia ${synapse_user_count}" >&2
post_status="error"
fi
echo "mas_syn2mas_migration: postcondicion: mas_users=${mas_user_count} synapse_users=${synapse_user_count} status=${post_status}" >&2
# =========================================================================
# PASO 7: Emitir JSON final
# =========================================================================
local dry_run_log_json apply_log_json
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
echo "{\"status\":\"${post_status}\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":${users_migrated},\"duration_s\":${duration_s}}"
return 0
}
# Ejecutar si se llama directamente (no sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
mas_syn2mas_migration "$@"
fi
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Tests para mas_syn2mas_migration
# Verifica arg parsing sin conectar al VPS real.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/mas_syn2mas_migration.sh"
PASS=0
FAIL=0
assert_exit() {
local test_name="$1" expected_exit="$2"
shift 2
local actual_exit=0
set +e
"$@" >/dev/null 2>&1
actual_exit=$?
set -e
if [[ "$actual_exit" == "$expected_exit" ]]; then
echo "PASS: $test_name"
((PASS++)) || true
else
echo "FAIL: $test_name — expected exit $expected_exit, got $actual_exit"
((FAIL++)) || true
fi
}
assert_stdout_contains() {
local test_name="$1" needle="$2"
shift 2
local output actual_exit=0
set +e
output=$("$@" 2>/dev/null)
actual_exit=$?
set -e
if echo "$output" | grep -q "$needle"; then
echo "PASS: $test_name"
((PASS++)) || true
else
echo "FAIL: $test_name — expected stdout to contain '$needle', got: $output"
((FAIL++)) || true
fi
}
# Test: aborta con error cuando faltan args obligatorios
assert_exit "aborta con error cuando faltan args obligatorios" 1 \
mas_syn2mas_migration
# Test: help no devuelve error
assert_exit "help no devuelve error" 0 \
mas_syn2mas_migration --help
# Test: argumento desconocido retorna exit 1
assert_exit "argumento desconocido retorna exit 1" 1 \
mas_syn2mas_migration --unknown-flag
# Test: max-conflicts invalido retorna exit 1
assert_exit "max-conflicts invalido retorna exit 1" 1 \
mas_syn2mas_migration \
--ssh-host fake-host \
--mas-container fake-container \
--synapse-config-path /fake/homeserver.yaml \
--log-dir "/tmp/test_mas_migration_$$" \
--max-conflicts "not-a-number"
# Test: help emite JSON valido con status=help
assert_stdout_contains "help emite JSON con status help" '"status":"help"' \
mas_syn2mas_migration --help
# Test: falta --ssh-host emite JSON con status=error
assert_stdout_contains "falta ssh-host emite JSON error" '"status":"error"' \
mas_syn2mas_migration \
--mas-container fake-container \
--synapse-config-path /fake/homeserver.yaml \
--log-dir "/tmp/test_mas_migration_$$"
# Test: falta --log-dir emite JSON con status=error
assert_stdout_contains "falta log-dir emite JSON error" '"status":"error"' \
mas_syn2mas_migration \
--ssh-host fake-host \
--mas-container fake-container \
--synapse-config-path /fake/homeserver.yaml
# Limpieza
rm -rf "/tmp/test_mas_migration_$$" 2>/dev/null || true
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
@@ -32,16 +32,16 @@ file_path: "bash/functions/infra/pre_commit_hook_install.sh"
source bash/functions/infra/pre_commit_hook_install.sh
# Instalar en el repo actual
pre_commit_hook_install /home/lucas/fn_registry
# INSTALLED /home/lucas/fn_registry/.git/hooks/pre-commit
pre_commit_hook_install $HOME/fn_registry
# INSTALLED $HOME/fn_registry/.git/hooks/pre-commit
# Idempotente: segunda llamada no sobreescribe
pre_commit_hook_install /home/lucas/fn_registry
# SKIP /home/lucas/fn_registry/.git/hooks/pre-commit (already installed)
pre_commit_hook_install $HOME/fn_registry
# SKIP $HOME/fn_registry/.git/hooks/pre-commit (already installed)
# Forzar reinstalacion (hace backup del hook anterior)
pre_commit_hook_install /home/lucas/fn_registry --force
# INSTALLED /home/lucas/fn_registry/.git/hooks/pre-commit
pre_commit_hook_install $HOME/fn_registry --force
# INSTALLED $HOME/fn_registry/.git/hooks/pre-commit
```
## Notas
@@ -58,5 +58,5 @@ Si no puede localizar `fn_registry`, el hook imprime un aviso y sale con exit 0
Configurar `FN_REGISTRY_ROOT` en el perfil del shell para garantizar que el hook siempre encuentre el registry:
```bash
export FN_REGISTRY_ROOT=/home/lucas/fn_registry
export FN_REGISTRY_ROOT=$HOME/fn_registry
```
+3 -3
View File
@@ -28,13 +28,13 @@ output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En
```bash
# Desde dentro de cpp/apps/chart_demo/
cd /home/lucas/fn_registry/cpp/apps/chart_demo
cd $HOME/fn_registry/cpp/apps/chart_demo
resolve_cpp_app_dir
# -> chart_demo\t/home/lucas/fn_registry/cpp/apps/chart_demo
# -> chart_demo\t$HOME/fn_registry/cpp/apps/chart_demo
# Con argumento explicito
resolve_cpp_app_dir registry_dashboard
# -> registry_dashboard\t/home/lucas/fn_registry/cpp/apps/registry_dashboard
# -> registry_dashboard\t$HOME/fn_registry/cpp/apps/registry_dashboard
# Capturar los dos campos
resolved=$(resolve_cpp_app_dir graph_explorer)
+1 -1
View File
@@ -7,7 +7,7 @@
resolve_cpp_app_dir() {
local app_arg="${1:-}"
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
local root="${FN_REGISTRY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
_list_cpp_apps() {
ls "$root/apps/" 2>/dev/null | sed 's/^/ apps\//'
+1 -1
View File
@@ -39,7 +39,7 @@ echo "$result"
# {"files_transferred": 12, "total_size": "1.23 MB", "ssh_alias": "prod-server", "remote_dir": "/opt/apps/dag_engine"}
# Deploy con ruta absoluta local
rsync_deploy "/home/lucas/fn_registry/apps/myapp/" "myserver" "/opt/myapp"
rsync_deploy "$HOME/fn_registry/apps/myapp/" "myserver" "/opt/myapp"
```
## Notas
@@ -0,0 +1,73 @@
---
name: start_nordvpn_socks_bridge
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "start_nordvpn_socks_bridge([--port N] [--socks-host HOST] [--socks-port N] [--user U] [--pass P]) -> JSON"
description: "Levanta un proxy HTTP local sin auth que reenvía al servidor SOCKS5 de NordVPN con auth usando gost v3. Resuelve la limitación de Chrome, que no soporta SOCKS5-con-auth: el navegador apunta a http://127.0.0.1:<port> (sin auth) y el tráfico sale por NordVPN. Idempotente: si el puerto ya escucha, no relanza."
tags: [navegator, vpn, proxy, nordvpn, socks5, gost, chrome, cdp]
params:
- name: "--port"
desc: "Puerto HTTP local del bridge (default 8889)"
- name: "--socks-host"
desc: "Servidor SOCKS5 de NordVPN (default socks-nl1.nordvpn.com)"
- name: "--socks-port"
desc: "Puerto del servidor SOCKS5 (default 1080)"
- name: "--user"
desc: "Service username de NordVPN. Si se omite, lee NORDVPN_SOCKS_USER del entorno"
- name: "--pass"
desc: "Service password de NordVPN. Si se omite, lee NORDVPN_SOCKS_PASS del entorno"
output: "JSON en stdout: {proxy_url, pid, socks_host, status}. status puede ser 'running' (lanzado ahora) o 'already_running' (puerto ya escuchaba). Errores a stderr + exit 1."
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/start_nordvpn_socks_bridge.sh"
---
## Ejemplo
```bash
# Con env vars (recomendado para scripts):
NORDVPN_SOCKS_USER=xxx NORDVPN_SOCKS_PASS=yyy \
bash bash/functions/infra/start_nordvpn_socks_bridge.sh \
--port 8889 \
--socks-host socks-nl1.nordvpn.com
# Salida (primera vez):
# {"proxy_url":"http://127.0.0.1:8889","pid":12345,"socks_host":"socks-nl1.nordvpn.com","status":"running"}
# Salida (idempotente, ya corría):
# {"proxy_url":"http://127.0.0.1:8889","pid":null,"socks_host":"socks-nl1.nordvpn.com","status":"already_running"}
# Luego Chrome (o el flujo CDP del navegator) apunta al bridge:
# chrome.exe --proxy-server=http://127.0.0.1:8889
# Verificar que el tráfico sale por NordVPN:
# curl -x http://127.0.0.1:8889 https://api.ipify.org
# -> 109.202.99.x (IP NordVPN NL, no la IP de casa)
```
## Cuando usarla
Cuando necesitas que Chrome (o cualquier app que solo acepta proxy HTTP sin auth) salga por NordVPN, pero NordVPN solo ofrece SOCKS5-con-auth. Chrome no soporta SOCKS5-with-authentication — este bridge actúa de intermediario sin auth local. Útil especialmente en el flujo CDP del navegator (cdp-cli + agente browser) cuando quieres que el browser de automatización salga con IP NordVPN para evadir DPI del ISP o geo-bloqueos, sin exponer las credenciales NordVPN al proceso del browser.
## Gotchas
- **NordVPN SOCKS5 exige service credentials**, no el usuario/contraseña de la cuenta NordVPN. Se obtienen en dashboard.nordvpn.com → Manual Setup → Service credentials.
- **Chrome no soporta SOCKS5-auth** nativamente (a diferencia de Firefox que sí). Por eso este bridge HTTP-sin-auth es necesario.
- **gost escucha en todas las interfaces** (`-L http://:PORT`). El puerto local NO tiene autenticación. No exponer en redes no confiables. Para bind solo a loopback cambiar el flag a `-L http://127.0.0.1:PORT` en el script si es necesario.
- **Servidores NordVPN SOCKS5 disponibles**: `socks-nl1..8.nordvpn.com`, `socks-de1..4.nordvpn.com`, `socks-us1..8.nordvpn.com`, etc. La lista completa en el dashboard de NordVPN.
- **Si gost no está instalado**: se descarga automáticamente `gost v3.0.0 linux amd64` a `~/.local/bin/gost`. Requiere curl y tar.
- **Log**: en `/tmp/nordvpn_socks_bridge_<port>.log`. Consultar si el bridge no arranca.
- **PID null en already_running**: cuando el puerto ya escuchaba, el PID del proceso no se recupera (habría que hacer `lsof`/`ss` para identificarlo).
- **Consumidor principal**: flujo `navegator`/CDP — ver `docs/capabilities/navegator.md`. El agente browser lanza este bridge antes de abrir Chrome con `--proxy-server=http://127.0.0.1:<port>`.
- **Gotcha invocación desde el Bash tool de Claude (exit 144)**: el script deja gost en background (`nohup ... & disown`); ese daemon retiene el pipe de stdout del tool → el harness mata el proceso con SIGSTKFLT (exit 144) AUNQUE el bridge SÍ arranca bien. Lanzar con `run_in_background:true` o redirigiendo todo (`>/tmp/x 2>&1 </dev/null`) para evitarlo. En terminal real (o `fn run` interactivo) no ocurre. Verificado 2026-05-30: el bridge queda corriendo y funcional pese al 144.
- **Windows→WSL**: si Chrome corre en Windows (chrome.exe) y gost en WSL2, Chrome alcanza `127.0.0.1:<port>` vía localhostForwarding de WSL2. Verificar con `curl -x http://127.0.0.1:<port> https://api.ipify.org` desde ambos lados.
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# start_nordvpn_socks_bridge — Levanta proxy HTTP local sin auth que reenvía a SOCKS5 NordVPN con auth via gost v3.
# Resuelve la limitacion de Chrome que no soporta SOCKS5-con-auth.
set -euo pipefail
# --- defaults ---
PORT=8889
SOCKS_HOST="socks-nl1.nordvpn.com"
SOCKS_PORT=1080
VPN_USER="${NORDVPN_SOCKS_USER:-}"
VPN_PASS="${NORDVPN_SOCKS_PASS:-}"
# --- parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--port) PORT="$2"; shift 2 ;;
--socks-host) SOCKS_HOST="$2"; shift 2 ;;
--socks-port) SOCKS_PORT="$2"; shift 2 ;;
--user) VPN_USER="$2"; shift 2 ;;
--pass) VPN_PASS="$2"; shift 2 ;;
*) echo "Unknown arg: $1" >&2; exit 1 ;;
esac
done
# --- validate creds ---
if [[ -z "$VPN_USER" ]]; then
echo "error: NORDVPN_SOCKS_USER not set. Use --user or export NORDVPN_SOCKS_USER" >&2
exit 1
fi
if [[ -z "$VPN_PASS" ]]; then
echo "error: NORDVPN_SOCKS_PASS not set. Use --pass or export NORDVPN_SOCKS_PASS" >&2
exit 1
fi
LOG_FILE="/tmp/nordvpn_socks_bridge_${PORT}.log"
# --- check idempotencia: ya escucha? ---
if ss -ltn 2>/dev/null | grep -q ":${PORT} "; then
echo "{\"proxy_url\":\"http://127.0.0.1:${PORT}\",\"pid\":null,\"socks_host\":\"${SOCKS_HOST}\",\"status\":\"already_running\"}"
exit 0
fi
# --- asegurar gost ---
GOST_BIN=""
if command -v gost &>/dev/null; then
GOST_BIN="$(command -v gost)"
elif [[ -x "$HOME/.local/bin/gost" ]]; then
GOST_BIN="$HOME/.local/bin/gost"
else
echo "gost not found, downloading v3.0.0 linux amd64..." >&2
GOST_URL="https://github.com/go-gost/gost/releases/download/v3.0.0/gost_3.0.0_linux_amd64.tar.gz"
TMP_DIR="$(mktemp -d)"
curl -fsSL "$GOST_URL" -o "$TMP_DIR/gost.tar.gz" >&2
tar -xzf "$TMP_DIR/gost.tar.gz" -C "$TMP_DIR" >&2
mkdir -p "$HOME/.local/bin"
cp "$TMP_DIR/gost" "$HOME/.local/bin/gost"
chmod +x "$HOME/.local/bin/gost"
rm -rf "$TMP_DIR"
GOST_BIN="$HOME/.local/bin/gost"
echo "gost installed to $GOST_BIN" >&2
fi
# --- url-encode user y pass (puede tener caracteres especiales) ---
url_encode() {
python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=''))" "$1"
}
ENC_USER="$(url_encode "$VPN_USER")"
ENC_PASS="$(url_encode "$VPN_PASS")"
# --- lanzar gost en background ---
nohup "$GOST_BIN" \
-L "http://:${PORT}" \
-F "socks5://${ENC_USER}:${ENC_PASS}@${SOCKS_HOST}:${SOCKS_PORT}" \
>"$LOG_FILE" 2>&1 &
GOST_PID=$!
disown $GOST_PID
# --- esperar ~2s y verificar que el puerto escucha ---
sleep 2
if ! ss -ltn 2>/dev/null | grep -q ":${PORT} "; then
echo "error: gost did not start. Last lines of $LOG_FILE:" >&2
tail -10 "$LOG_FILE" >&2
exit 1
fi
echo "{\"proxy_url\":\"http://127.0.0.1:${PORT}\",\"pid\":${GOST_PID},\"socks_host\":\"${SOCKS_HOST}\",\"status\":\"running\"}"
+68
View File
@@ -0,0 +1,68 @@
---
name: wg_client_install
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_client_install(config_path_or_stdin, [interface_name]) -> json"
description: "Device-side: instala wg0.conf en /etc/wireguard/, habilita systemd wg-quick@wg0, verifica handshake con hub. Idempotente. Acepta config por path o stdin (para pipes desde wg_client_config)."
tags: [wireguard, client, install, mesh, systemd]
uses_functions: [wg_install_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: config_path_or_stdin
desc: "path al archivo .conf existente, o '-' para leer de stdin (compatible con pipe desde wg_client_config)"
- name: interface_name
desc: "nombre de la interfaz WireGuard (default: wg0). Determina /etc/wireguard/<iface>.conf y la unit systemd wg-quick@<iface>"
output: "JSON {status, interface, hub_endpoint, handshake_seen}. status: installed | already-configured | installed-no-handshake | installed-no-systemd"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/wg_client_install.sh"
---
## Ejemplo
```bash
source bash/functions/infra/wg_client_install.sh
# Desde pipe (caso más común en flow 0009):
wg_client_config_go_infra | jq -r '.INI' | wg_client_install -
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Desde archivo .conf generado previamente:
wg_client_install /tmp/peer_laptop.conf
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Con interfaz personalizada:
wg_client_install /tmp/peer_laptop.conf wg1
# {"status":"installed","interface":"wg1","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Segunda ejecución con misma config (idempotente):
wg_client_install /tmp/peer_laptop.conf
# {"status":"already-configured","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":false}
```
## Cuando usarla
Cuando necesites conectar un nuevo peer al mesh WireGuard en el flow 0009. Úsala justo después de `wg_client_config` (que genera el .conf) para instalarlo en el device peer. Es el paso final del onboarding de un nodo: config generada → instalada → verificada con handshake.
## Gotchas
- **Requiere root/sudo** para escribir en `/etc/wireguard/`, hacer `chmod 600`, y ejecutar `systemctl`. El operador debe tener `sudo` sin password para estos comandos, o ejecutar la función como root.
- **Idempotente por contenido**: si `/etc/wireguard/<iface>.conf` ya existe con el mismo contenido, retorna `status=already-configured` sin tocar nada. Si el contenido difiere, hace backup automático con timestamp antes de sobreescribir.
- **NetworkManager**: si NM gestiona la interfaz wg0, `wg-quick` puede fallar con conflicto. Solución: crear `/etc/NetworkManager/conf.d/99-wg.conf` con `[keyfile]\nunmanaged-devices=interface-name:wg0` y reiniciar NM antes de ejecutar esta función.
- **WSL2 sin systemd** (variantes antiguas o sin `/etc/wsl.conf` con `[boot] systemd=true`): `systemctl` no está disponible. La función detecta esto, emite `status=installed-no-systemd` con instrucciones en stderr para levantar la interfaz manualmente con `sudo wg-quick up wg0`. Para autostart en WSL2 sin systemd: añadir `sudo wg-quick up wg0` al final de `~/.bashrc`.
- **WSL2 con systemd**: kernel WSL2 >= 5.6 (default en distros recientes) incluye WireGuard built-in. Habilitar systemd en WSL2 con `[boot]\nsystemd=true` en `/etc/wsl.conf` y reiniciar WSL. Luego esta función funciona igual que en Linux nativo.
- **Android / Termux**: NO usar esta función. Termux no tiene systemd ni `/etc/wireguard/`. En Android usar la app WireGuard oficial (F-Droid / Play Store) e importar el .conf generado por `wg_client_config` directamente desde la app.
- **handshake_seen=false con status=installed-no-handshake**: la interfaz está activa pero el hub no ha respondido en 10s. No es un error fatal — puede tardar más si el hub está ocupado o hay NAT traversal pendiente. Verificar: endpoint accesible por UDP, hub corriendo con `wg show`, claves public/preshared coincidentes.
- Los logs van siempre a stderr con prefijo `[wg_client_install]`; stdout es exclusivamente el JSON de resultado.
## Capability growth log
<!-- Rellenar solo cuando haya version bump real -->

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