191 Commits

Author SHA1 Message Date
egutierrez 18bdfc7bfd chore(issues): cerrar issue 0076 — gradle_run android-sdk detection fixed
Co-Authored-By: fn-orquestador <noreply@anthropic.com>
2026-05-15 14:01:43 +02:00
egutierrez 27ae829a1e fix(infra): gradle_run detecta android-sdk (install_android_sdk default) en orden correcto
ANDROID_HOME resolution ahora busca en orden:
  1. $HOME/android-sdk  — path que instala install_android_sdk_bash_infra
  2. $HOME/Android/Sdk  — default Android Studio Linux
  3. WSL2 Windows path  — $ANDROID_SDK_WIN o /mnt/c/Users/$USER/.../Android/Sdk

Cada candidato se valida con platform-tools/ presente (no solo directorio raiz).

Fix: issue 0076

Co-Authored-By: fn-orquestador <noreply@anthropic.com>
2026-05-15 14:01:36 +02:00
egutierrez 88119ee1b2 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 282c2e3ba8 Merge quick/issue-0094-doc 2026-05-14 18:08:19 +02:00
egutierrez 950b994797 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 23f5f1c25f 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 be8a61e724 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 80f44cc89e Merge issue 0091: kanban sidebar drag zones 2026-05-14 13:58:23 +02:00
egutierrez 188122812a 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 e2ecdc7533 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 7d82359a45 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 4e8b5af6c4 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 cfdf515228 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 d110aa40f9 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 aec5d82011 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 88b5b27dc0 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 574b3f6823 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 552c40bc42 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 1702f12664 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 a802f59f55 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 ef60449e64 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 c7904a7dcb 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 b4c28da2ba 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 2297edf2ab 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 9d0a1d99e8 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 a396ee781a 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 42c14fae59 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 bd036cf3d4 Merge branch 'issue/0005-fix-layout-dock-restore' (cfg.pre_frame hook) 2026-05-10 14:21:32 +02:00
egutierrez b5fc99c2fa 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 401d8523b4 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 b8dd7ea018 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 da61fa4d47 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 aca2348a20 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 4b9698b1b7 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 bf78a8c9be 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 f851a63742 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 783a232104 merge: 0064 registry_mcp MCP server 2026-05-09 13:31:29 +02:00
egutierrez 5bd0862d8c 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 aceb10b672 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 416b15786d 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 83c16d81b4 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 8618aa1be3 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 4d5a5bd3ea 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 793481bb11 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 c3fe61818e 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 1ffedbf48d 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 c9bb356ffe 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 fc627930f9 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 c2d156a8fb 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 c149ea161f 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 7490336709 chore: auto-commit (1 archivos)
- apps/dag_engine/app.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:11:14 +02:00
egutierrez 75714c9007 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 625569485f 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 c0e0ceadd8 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 32fc9c725b 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 c5f1b55a8e 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 76dcb05bd3 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 046f3ab2cb 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 5bee3d813f 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 5194de3c04 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 1e8ade0ed4 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 b4db4e4ef5 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 dabc945eda 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 f5c651d1f1 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 3b3378cfc1 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 e72d6364d4 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 7894a3d54a 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 ea899daa14 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 7b0384c804 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 d115d8e830 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 07d06d5e7d 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 b04bb846c7 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 3de82c53c1 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 80e1076d99 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 46ac1ee031 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 a028928bc7 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 71f55e0c17 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 81d8a7c95d 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 6249e01419 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 f102aba952 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 471e14caf7 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 563c6c7677 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 bfc93d6997 merge: issue/0040-hybrid-extraction-pipeline — pipeline hibrido extraccion grafos 2026-04-30 16:53:31 +02:00
egutierrez e6451b4912 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 1a3538785c 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 ada9b96765 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 ddf45c6e41 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 7761740d53 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 0076870e99 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 e1f41b263d 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 829bd64aaa 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 dff0c0d2b7 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 2341a4a0ca 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 fea3cdad5d 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 fa5bcca155 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 fa9b1d449d 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 0a76353a13 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 b10c545479 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 428b203e53 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 e3a84b1635 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 6526da32dc 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 0904409c59 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 25a809e3eb 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 336051fef5 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 c1396db84d merge: issue/0049j-graph-labels — graph_labels + LabelPolicy + ImDrawList overlay 2026-04-29 23:53:35 +02:00
egutierrez 1861205504 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 313f857c23 merge: issue/0049i-graph-layouts-static — graph_layouts + viewport multi-select+lasso 2026-04-29 23:42:44 +02:00
egutierrez 7644a50d00 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 bbce9541c9 merge: issue/0049h-graph-force-layout-gpu — graph_force_layout_gpu compute + spatial hash 2026-04-29 23:29:21 +02:00
egutierrez 35312ea66e 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 982a9f9a2b merge: issue/0049g-graph-source-operations — graph_sources lector operations.db + streaming 2026-04-29 23:12:43 +02:00
egutierrez 54cee13e8e 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 777b071ef8 merge: issue/0049f-graph-renderer-symbols — renderer shapes/iconos/flechas/edge-styles 2026-04-29 23:01:54 +02:00
egutierrez ac11300335 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 eb2078ac9a merge: issue/0049e-graph-types-extended — graph_types modelo extendido + EntityType/RelationType + flags 2026-04-29 22:44:44 +02:00
egutierrez b9ffc13caf 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 a6e3298f1b merge: issue/0049d-graph-edges-vertex-pulling — graph_renderer aristas via vertex pulling + TBO 2026-04-29 22:33:00 +02:00
egutierrez 79b5f0b194 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 9a2fe5349b merge: issue/0049c-graph-renderer-tier1 — RGBA8 + orphan + frustum cull + auto-pause helper 2026-04-29 22:17:22 +02:00
egutierrez 427262b892 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 97725e0641 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 32e58556fa 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 ebc012a5db 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 2124f6be07 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 9904d5cd63 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 7c09255c8a 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 6dd1fe07bd 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 9b2745fa25 feat(cpp/framework): viewports=true por defecto en AppConfig — ventanas arrastrables fuera del main 2026-04-29 00:54:43 +02:00
egutierrez cbe162630c fix(primitives_gallery): Windows mkdir() solo acepta el path en --capture 2026-04-29 00:31:53 +02:00
egutierrez 63fbbb9cd0 docs(issues): cerrar 0046 — actualizar README 2026-04-29 00:31:10 +02:00
egutierrez 6d70160919 merge: issue/0046-cpp-refactor-raw-imgui — implementación paralela 2026-04-29 00:30:36 +02:00
egutierrez f906ffbec4 docs: cerrar issue 0046 2026-04-29 00:29:54 +02:00
egutierrez 2aceccfd7e 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 cd445d8e1a 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 aeec68a552 docs(issues): cerrar 0048 — actualizar README 2026-04-29 00:20:13 +02:00
egutierrez 70a996d654 merge: issue/0048-cpp-visual-tests-ci-gate — implementación paralela 2026-04-29 00:19:37 +02:00
egutierrez a03da106a6 docs: cerrar issue 0048 2026-04-29 00:18:58 +02:00
egutierrez 33aace3686 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 cbc0714c80 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 405ceacb0a 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 13b12e2471 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 ea0c00c4f8 docs(issues): cerrar 0043 — actualizar README 2026-04-29 00:10:25 +02:00
egutierrez f4acd56694 merge: issue/0043-cpp-apps-standardize-shell — implementación paralela 2026-04-29 00:09:43 +02:00
egutierrez a5c721655e docs: cerrar issue 0043 2026-04-29 00:08:56 +02:00
egutierrez 015cf290eb refactor(shaders_lab): usar AppConfig.panels + layouts_cb (issue 0043) 2026-04-29 00:08:40 +02:00
egutierrez cbf8fd911f refactor(primitives_gallery): usar AppConfig.about + init_gl_loader (issue 0043) 2026-04-29 00:06:51 +02:00
egutierrez 503ccf30f4 refactor(chart_demo): usar AppConfig.about (issue 0043) 2026-04-29 00:06:20 +02:00
egutierrez 1e25617ae1 docs(issues): cerrar 0045 — actualizar README 2026-04-29 00:00:56 +02:00
egutierrez 9a4a86d317 merge: issue/0045-cpp-extract-pure-logic — implementación paralela 2026-04-28 23:59:45 +02:00
egutierrez 1506c646a0 docs: cerrar issue 0045 2026-04-28 23:59:08 +02:00
egutierrez 00d18d38b6 test(cpp): tests para sql_parse, process_state_machine, file_poll_diff 2026-04-28 23:58:40 +02:00
egutierrez 5044528175 refactor(shaders_lab): extraer compile_* a compiler.{h,cpp} 2026-04-28 23:56:33 +02:00
egutierrez b5058c56fe refactor(cpp/core): file_watcher usa file_poll_diff 2026-04-28 23:55:11 +02:00
egutierrez 13339abdb3 feat(cpp/core): añadir file_poll_diff pure 2026-04-28 23:53:49 +02:00
egutierrez ad254beeac refactor(cpp/core): process_runner usa process_state_machine 2026-04-28 23:53:08 +02:00
egutierrez 7779ce7b46 feat(cpp/core): añadir process_state_machine pure 2026-04-28 23:52:37 +02:00
egutierrez e70d0940a4 refactor(cpp/core): sql_workbench usa sql_parse 2026-04-28 23:51:59 +02:00
egutierrez dd3f73905f feat(cpp/core): añadir sql_parse pure 2026-04-28 23:51:23 +02:00
egutierrez b2d7b29e00 docs(issues): cerrar 0041, 0042, 0044, 0047 + actualizar README 2026-04-28 23:45:40 +02:00
egutierrez 50c7452df3 merge: issue/0047-cpp-tests-foundation — implementación paralela 2026-04-28 23:44:55 +02:00
egutierrez f62392179f merge: issue/0044-cpp-orphans-audit — implementación paralela 2026-04-28 23:44:52 +02:00
egutierrez 41e66f60df merge: issue/0042-cpp-layout-storage-public — implementación paralela 2026-04-28 23:44:44 +02:00
egutierrez 78dc004371 merge: issue/0041-cpp-app-best-practices — implementación paralela 2026-04-28 23:44:24 +02:00
egutierrez 6b8f0dc10e chore(cpp): script run_tests.sh para build+ctest one-shot 2026-04-28 23:42:38 +02:00
egutierrez 3699a2554d 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 715074c2e8 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 f858f3a9fc 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 557ec658c9 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 6123c87483 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 0e27401e03 docs: cerrar issue 0042 2026-04-28 23:41:19 +02:00
egutierrez 8c7311b70d 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 e4f86594f0 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 0adb5eeaa6 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 958189227d 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 96fcd05511 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 08cc179ca8 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 e356b7ac42 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 914372a517 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 8afdedf793 docs(issues): añadir 0041-0048 — refactor C++ apps, tests, primitives standarization 2026-04-28 23:38:56 +02:00
egutierrez 3e0d3d612a 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 10e0b712ca feat(cpp/framework): extender AppConfig con about, panels, layouts_cb, init_gl_loader 2026-04-28 23:37:23 +02:00
egutierrez c1b1d8fbad docs(cpp): añadir PATTERNS.md con checklist de apps 2026-04-28 23:34:07 +02:00
egutierrez 0cbc08723d 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 836ff02578 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 363fc07e74 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 edcf029c6d 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 02eed13913 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 5bbe45ca30 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 58c4bc5f05 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 b837b8281a 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 73e2f688b6 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
4338 changed files with 604507 additions and 2435 deletions
+162 -26
View File
@@ -2,57 +2,144 @@
Registry personal de codigo reutilizable con busqueda FTS. Diseñado para composicion funcional y agentes. Registry personal de codigo reutilizable con busqueda FTS. Diseñado para composicion funcional y agentes.
## Objetivos del registry (Norte) — Issues 0086 + 0087
**4 metricas optimizadas por el bucle reactivo** (visibles en Monitor tab del `registry_dashboard`):
1. **MAXIMIZAR `Reg %`** — porcentaje de calls del agente que golpean una funcion del registry (`function_id != ''`). Cada bash inline o heredoc que reescribe logica baja el ratio. Target: subir cada semana.
2. **MEJORAR uso del registry por Claude** — el agente debe encontrar y usar funciones existentes antes de escribir codigo. Indicadores: `MCP` (mcp/heredoc/fn run) sube; violations baja. Si Claude no encuentra una funcion por busqueda mediocre, mejorar `description`/`tags`/`params_schema` de esa funcion.
3. **ACELERAR tareas comunes via funciones nuevas** — patrones inline repetidos >2 veces -> `fn-constructor` crea la funcion, Claude la usa el siguiente turno. Velocidad medida en pasos (turnos) por tarea. Pattern detection: tab Monitor + `mcp__registry__fn_proposal action="list"`.
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry no crece inflando funciones, crece **promoviendo secuencias A→B(→C) que se repiten con exito** a pipelines one-shot. Hoy `bank_login + bank_make_transfer` (2 calls). Manana `bank_transfer_oneshot` (1 call). Misma capacidad, mitad de pasos. Detectado por telemetria de secuencias en `call_monitor`. Una funcion que hace bien UNA cosa NO necesita crecer — lo que crece es el catalogo de composiciones probadas.
**Auto-discovery zero-second-lookup:** cada `.md` debe ser autosuficiente — `## Ejemplo` lanzable + `## Cuando usarla` + `## Gotchas` (impuras). Descubrir = lanzar, sin segunda lectura. Ver `.claude/rules/function_growth_and_self_docs.md`.
Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. Ejemplo: un bash heredoc rapido hoy que reinventa logica = penaliza objetivos 1 y 3 manana.
**Dos bases de datos SQLite:** **Dos bases de datos SQLite:**
- **registry.db** (raiz) — funciones, tipos, proposals, apps, projects, analysis, vaults, pc_locations. Regenerable con `fn index` (excepto proposals y pc_locations). - **registry.db** (raiz) — funciones, tipos, proposals, apps, projects, analysis, vaults, pc_locations. Regenerable con `fn index` (excepto proposals y pc_locations).
- **operations.db** (por app en `apps/*/`) — entities, relations, executions, assertions. Datos vivos. - **operations.db** (por app en `apps/*/`) — entities, relations, executions, assertions. Datos vivos.
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token). **Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
**Reglas y convenciones:** ver `.claude/rules/INDEX.md` **Reglas y convenciones:** ver `.claude/rules/INDEX.md`
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
---
## Delegacion + Capability Groups (REGLA DURA — issue 0086)
Claude **multiplica capacidades** delegando creacion de funciones a `fn-constructor` y reusandolas inmediatamente. NO escribir logica reutilizable inline.
### Flujo obligatorio (mismo turno)
1. **Detectar gap**. Si vas a escribir >=5 lineas de logica reutilizable inline -> STOP.
2. **Spawn `fn-constructor`** via `Agent(subagent_type="fn-constructor", ...)`. Sin preguntar al usuario.
3. **Paralelo**: si hay >1 funcion independiente -> **una sola llamada al Agent tool con N tool_use blocks paralelos** en mismo mensaje. NO serializar.
4. **Tag de grupo obligatorio** (`notebook`, `metabase`, `deploy`, etc.). Ver `docs/capabilities/INDEX.md`.
5. **`fn index`** + **importar + invocar en mismo turno**. No dejar funcion huerfana recien creada.
6. **Auto-verificar**: `fn doctor uses-functions` + `fn doctor unused` si tocas >=3 funciones nuevas.
### Capability groups
Cluster de >=3 funciones que comparten dominio operativo. Cada grupo tiene tag plano + pagina madre `docs/capabilities/<grupo>.md` con: lista de funciones, ejemplo canonico end-to-end, fronteras.
**Antes de buscar funciones sueltas en una tarea de dominio conocido:** lee `docs/capabilities/<grupo>.md` para cargar el cluster entero en un solo read. Filtro MCP: `mcp__registry__fn_search query="" tag="<grupo>"`.
Reglas completas: `.claude/rules/delegation.md` + `.claude/rules/capability_groups.md`.
### Telemetria CAPABILITY-GROWTH
Cada turno el hook `UserPromptSubmit` inyecta `CAPABILITY-GROWTH: created_this_session=X used=Y orphan=Z`. Si `orphan>0` -> integra la funcion antes de cerrar turno o documenta por que.
--- ---
## Explorar el registry (OBLIGATORIO) ## Explorar el registry (OBLIGATORIO)
**SIEMPRE** consulta registry.db antes de escribir codigo, crear funciones, o responder sobre el registry. No uses grep/glob sobre archivos .go/.md — la BD es la fuente de verdad. **SIEMPRE** consulta registry.db antes de escribir codigo, crear funciones, o responder sobre el registry. No uses grep/glob sobre archivos .go/.md — la BD es la fuente de verdad.
**La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Estos campos tambien estan indexados en FTS5, asi que puedes buscar dentro del codigo y la documentacion directamente. Para leer el codigo de una funcion: `SELECT code FROM functions WHERE id = '...'`. Para leer su documentacion: `SELECT documentation FROM functions WHERE id = '...'`. ### Usa SIEMPRE el MCP `registry` (regla por defecto)
**Busquedas FTS5 obligatorias:** Usa SIEMPRE la tabla FTS5 para buscar tanto por `name` como por `description`. Esto encuentra coincidencias parciales y similares que una busqueda exacta perderia. Usa operadores FTS5: `OR` para ampliar, `*` para prefijos, `NEAR` para proximidad. **OBLIGATORIO:** para buscar/leer/inspeccionar el registry usa SIEMPRE las tools del MCP `registry`. NO uses `sqlite3` ni `Bash` para esto salvo que el MCP no exponga la consulta que necesitas.
```bash | Necesidad | Tool MCP |
# Busqueda FTS5 por nombre Y descripcion (USAR SIEMPRE ESTE PATRON) |---|---|
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slice OR description:slice') ORDER BY name;" | Buscar funciones/tipos/apps por texto (FTS5) | `mcp__registry__fn_search` |
| Ver una entrada concreta (functions, types, apps, ...) | `mcp__registry__fn_show` |
| Leer el codigo fuente de una funcion/tipo | `mcp__registry__fn_code` |
| Ver quien usa una funcion/tipo | `mcp__registry__fn_uses` |
| Listar dominios | `mcp__registry__fn_list_domains` |
| Ejecutar funcion/pipeline | `mcp__registry__fn_run` |
| Crear funcion nueva (scaffolding) | `mcp__registry__fn_create_function` |
| Diagnostico read-only (artefacts/services/sync/...) | `mcp__registry__fn_doctor` |
# FTS5 con prefijo (encuentra slice, slicing, sliced...) Razones: menos tokens, output estructurado, FTS5 escapado bien (sin gotchas de `column:"valor"`), permisos pre-aprobados, no requiere `cd` ni paths absolutos a `registry.db`.
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slic* OR description:slic*') ORDER BY name;"
# FTS5 en tipos **La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Tambien indexados en FTS5 — buscas dentro del codigo directamente. Para leer codigo: `mcp__registry__fn_code <id>`.
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:result OR description:result') ORDER BY name;"
# FTS5 por semantica de params (composabilidad) ### Ejemplos MCP (usa estos, NO sqlite3)
sqlite3 registry.db "SELECT id, json_extract(params_schema, '$.output') FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'params_schema:retornos');"
# Por dominio Cada llamada MCP se registra en `call_monitor` (issue 0085). Cada `sqlite3 registry.db "SELECT ..."` queda fuera del bucle reactivo y dispara el hook PreToolUse.
sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;"
# Puras de un dominio ```
sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;" # Busqueda basica por nombre/descripcion (FTS5 detras)
mcp__registry__fn_search query="slice"
# Tipos por dominio # Filtros: kind, purity, domain, lang
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';" mcp__registry__fn_search query="filter" kind="function" purity="pure" domain="core"
# Dependencias # Prefijo FTS5 — encuentra slice/slicing/sliced
sqlite3 registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE uses_functions != '[]';" mcp__registry__fn_search query="slic*"
# Proposals pendientes # Buscar tipos
sqlite3 registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending';" mcp__registry__fn_search query="result" entity="types"
# Schema completo # Apps
sqlite3 registry.db ".schema" mcp__registry__fn_search query="kanban" entity="apps"
# Listar dominios
mcp__registry__fn_list_domains
# Ver una entrada concreta (functions, types, apps, analysis, proposals...)
mcp__registry__fn_show id="filter_slice_go_core"
# Codigo fuente de una funcion/tipo
mcp__registry__fn_code id="filter_slice_go_core"
# Quien consume una funcion (consumidores indexados via uses_functions)
mcp__registry__fn_uses id="filter_slice_go_core"
# Proposals (pending, approved, ...)
mcp__registry__fn_proposal action="list" status="pending"
mcp__registry__fn_proposal action="show" id="<proposal_id>"
# Diagnostico read-only del registry (artefacts/services/sync/uses-functions/unused/cpp-apps)
mcp__registry__fn_doctor subcommand="artefacts"
mcp__registry__fn_doctor subcommand="sync"
``` ```
**Regla:** Si necesitas saber si algo existe o hay algo similar, haz la consulta FTS5 sobre la BD. No asumas que no existe sin consultar primero. **Escapado FTS5 (gotcha cuando pasas query libre):** valores con `-`, `.`, `:`, espacios rompen el parser FTS5 si los expones como `column:valor`. El MCP escapa por defecto, pero si construyes una `query` con sintaxis FTS5 explicita, encierra el valor en comillas dobles:
```
# MAL: query="description:single-page" -> "no such column: page"
# BIEN
mcp__registry__fn_search query='description:"single-page" OR description:"embed.FS"'
mcp__registry__fn_search query='description:"react router"'
```
### Excepciones autorizadas para sqlite3 directo
`sqlite3 registry.db` SOLO es legitimo si el MCP no expone la consulta:
- Introspeccion de schema: `.schema`, `.tables`, `PRAGMA table_info(...)`, `PRAGMA index_list(...)`.
- Agregaciones: `COUNT(*)`, `GROUP BY`, `SUM(...)`, `AVG(...)`.
- JOINs custom entre tablas (ej. `functions JOIN unit_tests ON ...`) no expuestos por el MCP.
Cualquier `SELECT ... FROM functions/types/apps/proposals WHERE ...` plano se hace via MCP. El hook PreToolUse avisa si ve `sqlite3 registry.db "SELECT ..."`.
### Schema rapido ### Schema rapido
@@ -73,7 +160,7 @@ sqlite3 registry.db ".schema"
- `entity_type`: app, analysis, project, vault - `entity_type`: app, analysis, project, vault
- `status`: active, missing, archived - `status`: active, missing, archived
- Se puebla con `fn sync`, NO con `fn index` - Se puebla con `fn sync`, NO con `fn index`
- Consultas: `SELECT * FROM pc_locations WHERE pc_id = 'home-wsl'` - Consultas: `mcp__registry__fn_doctor subcommand="sync"` (drift PC vs disco) o `sqlite3 registry.db "SELECT ... GROUP BY pc_id"` SOLO para agregaciones que el MCP no expone
**FTS5 (columnas buscables):** **FTS5 (columnas buscables):**
- `functions_fts`: id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema - `functions_fts`: id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema
@@ -82,6 +169,43 @@ sqlite3 registry.db ".schema"
--- ---
## Como invocar funciones del registry (CANONICO)
Tres patrones, uno por caso de uso. Toda invocacion del agente se loguea en `projects/fn_monitoring/apps/call_monitor/operations.db` para alimentar el bucle reactivo (issue 0085).
| Caso de uso | Patron canonico | Cuando usar |
|---|---|---|
| **Inspeccionar** registro (buscar, leer codigo, ver dependencias, dominios) | `mcp__registry__fn_search` / `fn_show` / `fn_code` / `fn_uses` / `fn_list_domains` | SIEMPRE para descubrimiento. Reemplaza `sqlite3 registry.db "SELECT ..."` inline. |
| **Ejecutar** UNA funcion o pipeline con sus args | `mcp__registry__fn_run <id> [args]` (preferido) o `./fn run <id> [args]` (fallback CLI) | Cuando hay UN id conocido a lanzar. Despacho automatico por lenguaje. Salida estructurada. |
| **Componer** ad-hoc varias funciones con logica intermedia | Heredoc `python/.venv/bin/python3 - <<'PYEOF' ... PYEOF` IMPORTANDO funciones del registry | Solo cuando hay loops/conditionals/dispatch entre N funciones. Las funciones del registry **se importan**, no se reescriben. |
Regla decisiva: antes de cada bloque de codigo, decide caso. Si dudas entre 2 y 3, casi siempre es 2 (un MCP run con args). Si el caso 3 se repite con el mismo shape >5 veces entre sesiones, **es candidato a pipeline** en `python/functions/pipelines/`.
### Antipatrones prohibidos
| Patron | Por que es malo | Sustituir por |
|---|---|---|
| `sqlite3 registry.db "SELECT ..."` para buscar funciones/tipos | Salta MCP, FTS5 gotchas, sin trazabilidad. Hook PreToolUse ya avisa. | `mcp__registry__fn_search` |
| `python -c "import metabase; print(dir(metabase))"` o `help(metabase)` para descubrir helpers | La fuente de verdad es el registry, no el `__init__.py` | `mcp__registry__fn_search "metabase"` + `mcp__registry__fn_show <id>` |
| Heredoc que reescribe logica que ya existe como funcion del registry | Reinvento + perdida de capitalizacion | Buscar primero; si falta, delegar a `fn-constructor` (no escribir inline) |
| `client._http.request(...)` directo cuando hay wrapper en el registry | Salta validacion del wrapper y telemetria | Usar wrapper; si la firma no cubre el caso, proponer extension via `fn proposal add` |
| Scripts en `temp/` para composiciones que se repiten | Codigo se pierde y no se monitoriza | Pipeline en `python/functions/pipelines/` o pipeline Bash en `bash/functions/pipelines/` |
| Imports `from <pkg> import *` en heredoc | Imposible saber que funcion del registry se uso | Imports explicitos `from <domain> import <name1>, <name2>` |
Excepciones autorizadas para `sqlite3` directo (no requieren MCP): `.schema`, `.tables`, `PRAGMA table_info`, `COUNT(*) GROUP BY`, JOINs custom entre tablas que el MCP no expone.
### Trazabilidad y bucle reactivo
Hook `PostToolUse` en `.claude/settings.local.json` parsea cada comando Bash + cada `mcp__registry__*` y escribe en la `operations.db` del call_monitor. Datos consumidos por:
1. **Tab "Claude usage" en `registry_dashboard`** — top funciones, latencias, error rate, huerfanas con `calls_90d=0`.
2. **Fase MEJORAR del bucle reactivo** — patrones inline repetidos generan proposals `new_function` con evidencia (session_ids + snippets). Funciones con error_rate alto y muchas llamadas suben en prioridad de bugfix.
3. **Auditoria de reglas** — assertions sobre `violation_count`, `mcp_ratio`, `heredoc_repetition`. Si fallan critical → proposal "actualizar CLAUDE.md / prompt del agente".
Datos sensibles: solo se guarda `args_hash`, NUNCA valores concretos de argumentos.
---
## Estructura ## Estructura
``` ```
@@ -98,11 +222,13 @@ fn-registry/
registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones
fn_operations/ # Paquete Go: operations database (libreria) fn_operations/ # Paquete Go: operations database (libreria)
apps/ # Apps ejecutables (TUIs, CLIs, scripts) — codigo NO reutilizable, cada una con su operations.db apps/ # Apps ejecutables (TUIs, CLIs, scripts) — codigo NO reutilizable, cada una con su operations.db
cpp/apps/ # Apps C++ standalone (sin proyecto). Ej: chart_demo, shaders_lab. Indexadas igual que apps/
analysis/ # Exploraciones Jupyter independientes — cada una con su venv, MCP y kernel conectado al registry analysis/ # Exploraciones Jupyter independientes — cada una con su venv, MCP y kernel conectado al registry
cmd/fn/ # CLI principal cmd/fn/ # CLI principal
docs/ # Specs de diseño docs/ # Specs de diseño
docs/templates/ # Plantillas de frontmatter docs/templates/ # Plantillas de frontmatter
temp/ # Workspace efimero — pruebas, APIs, prototipos (gitignored, no indexado) temp/ # Workspace efimero — pruebas, APIs, prototipos (gitignored, no indexado)
<artefacto>/playground/ # Prototipo rapido dentro de un artefacto padre (analysis/app/proyecto). No se indexa
``` ```
--- ---
@@ -128,6 +254,16 @@ fn show <id>
fn add -k function # Template fn add -k function # Template
fn check params # Lista funciones sin params_schema fn check params # Lista funciones sin params_schema
# Doctor: diagnostico read-only del registry y artefactos
fn doctor # Corre todos los checks
fn doctor artefacts # git/venv/app.md/upstream de cada app y analysis
fn doctor services # apps tag 'service' + systemctl + puerto
fn doctor sync # drift pc_locations BD vs disco
fn doctor uses-functions # imports reales vs uses_functions del app.md
fn doctor unused # funciones del registry sin consumidores
fn doctor --json # salida JSON (cualquier subcomando)
# Ver .claude/rules/fn_doctor.md para mapeo subcomando → funcion + acciones derivadas.
# Ejecutar funciones y pipelines (fn run) # Ejecutar funciones y pipelines (fn run)
fn run <id_or_name> [args...] # Ejecuta por ID o nombre fn run <id_or_name> [args...] # Ejecuta por ID o nombre
fn run init_metabase --project test # Go pipeline (go run .) fn run init_metabase --project test # Go pipeline (go run .)
@@ -189,7 +325,7 @@ Entornos usados automaticamente:
## Añadir funciones ## Añadir funciones
1. Consulta la BD para verificar que no existe algo similar 1. `mcp__registry__fn_search query="<nombre|desc>"` para verificar que no existe algo similar
2. Crea dos archivos segun el lenguaje: 2. Crea dos archivos segun el lenguaje:
- Go: `functions/{domain}/{name}.go` + `.md` - Go: `functions/{domain}/{name}.go` + `.md`
- Python: `python/functions/{domain}/{name}.py` + `.md` - Python: `python/functions/{domain}/{name}.py` + `.md`
+289
View File
@@ -0,0 +1,289 @@
---
name: fn-analizador
description: "Agente analizador (Fase 4) del ciclo reactivo. Lee `e2e_checks` declarados en app.md, ejecuta la suite via `e2e_run_checks_go_infra`, evalua assertions activas, calcula drift de metricas vs historico, persiste resultado en `e2e_runs` de operations.db y devuelve veredicto caveman pass/fail. NO modifica codigo ni propone fixes — eso es trabajo de fn-mejorador (Fase 5)."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Analizador — Fase 4 del Ciclo Reactivo
Eres el agente analizador del fn_registry. Tu rol es **validar end-to-end** que una app funciona correctamente, **detectar regresiones** vs historico, y **persistir el veredicto** en operations.db. Trabajas despues de `fn-recopilador` (Fase 3): el confirma que datos operativos estan integros, tu confirmas que la app COMPLETA funciona.
NO escribes codigo nuevo. NO modificas funciones del registry. NO creas proposals — eso es trabajo de `fn-mejorador` (Fase 5). Tu output es **veredicto + evidencia**, nada mas.
---
## REGLA FUNDAMENTAL: el contrato esta en `app.md::e2e_checks`
Sin contrato no hay validacion. Si la app objetivo NO tiene `e2e_checks` declarado en su `app.md`, NO inventes checks. Reporta "sin contrato" y sugiere usar `fn-recopilador design-e2e <app_id>` para que se proponga uno.
Ver regla `.claude/rules/e2e_validation.md` y issue 0068.
---
## Input
Recibes un `app_id` o `dir_path` de la app a validar. Ejemplos:
- `kanban_go_tools`
- `apps/kanban`
- `graph_explorer_cpp_viz`
- `projects/osint_graph/apps/graph_explorer`
Opcionalmente:
- `triggered_by`: `manual` (default) | `git_push` | `cron` | `reactive_loop`
- `git_sha`: SHA actual si se invoca desde un hook
---
## Algoritmo
### 1. Resolver app
```bash
# Por id
sqlite3 /home/lucas/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>';"
```
Si no hay match → reportar y abortar.
### 2. Leer `e2e_checks` del `app.md`
```bash
# Extraer YAML del frontmatter
sed -n '/^---$/,/^---$/p' "<dir_path>/app.md" | head -n -1 | tail -n +2
```
Parsear `e2e_checks:`. Si esta vacio o no existe:
```
=== fn-analizador: <app_id> ===
SIN CONTRATO
app.md no declara e2e_checks. fn-analizador no puede validar.
Sugerencia: invocar fn-recopilador con `design-e2e <app_id>` para
generar bloque e2e_checks_suggested.
```
Y abortar.
### 3. Preparar `operations.db` de la app
```bash
APP_DIR="<dir_path>"
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"
fi
# Verificar tabla e2e_runs existe (migracion 005)
sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='e2e_runs';"
```
Si falta `e2e_runs`, re-aplicar migraciones via `fn ops init`.
Algunas apps usan BD propia (ej. `apps/kanban/kanban.db`) en vez de `operations.db`. Si `operations.db` no existe ni tras `fn ops init`, persiste el run en una BD efimera de `/tmp/<app>_e2e_runs.db` con la misma migracion. Reporta este detalle.
### 4. Ejecutar la suite
Hay dos caminos:
**Camino A — invocar funcion del registry (preferido):**
```bash
cd /home/lucas/fn_registry
./fn run e2e_run_checks_go_infra ...
```
Esto requiere CLI `fn run` con args estructurados. Si todavia no esta soportado:
**Camino B — ejecutar checks individualmente con bash + capturar resultados:**
Generar un programa Go ad-hoc en `/tmp/run_e2e_<id>.go` que:
1. Carga el YAML de `e2e_checks` (parsear con `gopkg.in/yaml.v3` o reusar parser del registry).
2. Construye `[]infra.E2ECheck`.
3. Llama `infra.E2ERunChecks(checks, dirPath)`.
4. Imprime `[]CheckResult` como JSON por stdout.
Ejemplo del programa ad-hoc:
```go
package main
import (
"encoding/json"
"fmt"
"os"
infra "fn-registry/functions/infra"
"gopkg.in/yaml.v3"
)
func main() {
data, _ := os.ReadFile(os.Args[1])
var checks []infra.E2ECheck
yaml.Unmarshal(data, &checks)
results, err := infra.E2ERunChecks(checks, os.Args[2])
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
json.NewEncoder(os.Stdout).Encode(results)
}
```
Ejecutar con:
```bash
cd /home/lucas/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"
```
Capturar fallos como warning checks adicionales.
### 6. Calcular drift de metricas
Para cada `pipeline_id` con executions historicas (>5 corridas), comparar duration_ms actual vs baseline p50/p95 usando `metrics_drift_go_datascience`. Si drift > umbral (default 0.30 = +30%), generar warning check.
```bash
sqlite3 "$APP_DB" "
SELECT pipeline_id, duration_ms FROM executions
WHERE status = 'success'
ORDER BY started_at DESC
LIMIT 50;"
```
### 7. Diff golden si aplica
Si `<app_dir>/tests/golden/` existe:
```bash
for golden in "$APP_DIR"/tests/golden/*.expected; do
actual="${golden%.expected}.actual"
if [ -f "$actual" ]; then
# Reusar golden_diff_go_core via programa ad-hoc o script bash con cmp
cmp -s "$golden" "$actual" && pass || fail
fi
done
```
### 8. Persistir `e2e_runs`
```bash
RUN_ID="run_$(openssl rand -hex 8)"
NOW=$(date +%s)
TOTAL=$(echo "$RESULTS_JSON" | jq 'length')
PASS=$(echo "$RESULTS_JSON" | jq '[.[] | select(.status=="pass")] | length')
FAIL=$(echo "$RESULTS_JSON" | jq '[.[] | select(.status=="fail")] | length')
WARN=$(echo "$RESULTS_JSON" | jq '[.[] | select(.severity=="warning" and .status=="fail")] | length')
STATUS=$( [ "$FAIL" -eq 0 ] && echo "pass" || ( [ "$PASS" -gt 0 ] && echo "partial" || echo "fail" ) )
sqlite3 "$APP_DB" "INSERT INTO e2e_runs
(id, app_id, started_at, finished_at, status, checks_total, checks_pass, checks_fail, checks_warn, summary_json, triggered_by, git_sha)
VALUES ('$RUN_ID', '$APP_ID', $START_TS, $NOW, '$STATUS', $TOTAL, $PASS, $FAIL, $WARN, json('$RESULTS_JSON'), '$TRIGGERED_BY', '$GIT_SHA');"
```
### 9. Veredicto caveman
Imprimir tabla con status por check, una linea cada uno:
```
=== fn-analizador: <app_id> ===
run_id: <RUN_ID>
status: <pass|fail|partial>
checks: <PASS>/<TOTAL> pass, <WARN> warn, <FAIL> fail
build_frontend ✓ 42s
build_backend ✓ 18s
migrations ✓ 0.4s
smoke_api ✓ 1.2s
tests_go ✗ 12s exit 1
FAIL: 3 of 45 tests failed
last error: kanban_test.go:127: expected 200, got 500
assertions ✓ 0 fails
metrics_drift ⚠ duration_ms p50 +47% vs ventana historica
next: fn-mejorador <app_id> --run-id <RUN_ID>
```
Caracteres: ✓ pass, ✗ fail critical, ⚠ warning fail, skip.
---
## Reglas de comportamiento
1. **Solo lectura sobre registry.db**. NO inserts/updates/deletes ahi.
2. **Escribe SOLO en `e2e_runs` y `assertion_results`** de operations.db de la app.
3. **No inventes checks**. Si `e2e_checks` esta vacio, abortar y sugerir `fn-recopilador design-e2e`.
4. **Cleanup obligatorio**. Si un check arranca un proceso en background (`cmd ... &`), matar el grupo de procesos al terminar la suite (`pkill -P $$` o usar `setsid`).
5. **Timeouts duros**. Cualquier check que exceda `timeout_s` se mata con `SIGKILL` y se reporta como `fail` con `Error: "timeout after Ns"`.
6. **No tocar produccion**. Las BDs efimeras van a `/tmp/`. Los puertos son altos (>8100). Si un check intenta tocar URLs externas que no sean test fixtures, marcalo warning y sigue.
7. **Idempotente**. Correr `fn-analizador` 10 veces seguidas debe dar 10 filas en `e2e_runs`, sin estado residual entre corridas.
8. **No depender de internet** salvo si el check lo declara explicitamente (ej. `enricher_fetch_webpage` toca `example.com`). En esos casos, `severity: warning` por default.
---
## Decisiones automaticas
- **Status global**:
- `pass` si todos los critical pasan (warnings ignorados para el global).
- `partial` si alguno paso pero hay un critical fail.
- `fail` si NINGUN check paso o si setup fallo.
- **Continue on fail**: por default sigue al siguiente check incluso si el actual fallo. Util para tener el cuadro completo. Excepcion: `build` fallido suele invalidar todos los siguientes — si el primer check con `id` empezando por `build` falla, marcar el resto como `skip` con `Error: "build failed, skipped"`.
- **Severity default**: `critical` si no se especifica.
- **Tiempo total**: si la suite supera 15 minutos, abortar con `partial` y reportar timeout global.
---
## Errores comunes
| Sintoma | Causa probable | Accion |
|---|---|---|
| `e2e_checks vacio` | App no tiene contrato | Sugerir `fn-recopilador design-e2e` |
| `migration 005 no aplicada` | operations.db viejo | `./fn ops init <app_dir>` |
| `port already in use` | Run anterior no limpio | `pkill -f <app_name>` antes de retry |
| `health timeout` | Servicio no levanta | Revisar build + migrations checks anteriores |
| `cmd not found` | Falta dependencia (pnpm, sqlite3) | Reportar warning, no fail critical |
| `permission denied: bash -c` | workDir mal | Verificar dir_path absoluto |
---
## Output canonico (stdout)
Devuelve SIEMPRE un bloque con:
1. Header `=== fn-analizador: <app_id> ===`
2. Linea `run_id: <id>`
3. Linea `status: <pass|partial|fail>`
4. Linea `checks: P/T pass, W warn, F fail`
5. Tabla con un check por linea (id ✓/✗/⚠ duration optional_error)
6. Linea final `next: fn-mejorador <app_id> --run-id <RUN_ID>` SI hay fails (orienta al humano/main thread).
Si setup fallo (no se pudo correr nada), output:
```
=== fn-analizador: <app_id> ===
SETUP FAIL
<razon>
```
---
## Composicion con otras fases
- **Antes de fn-analizador**: `fn-recopilador` audita integridad de operations.db. Si recopilador reporta FAIL critical, NO correr analizador (datos rotos invalidan la suite).
- **Despues de fn-analizador**: si hay fails → invocar `fn-mejorador` con el `run_id`. Si todo pass → terminar (suite verde, app deployable).
Cadena completa: `fn-executor → fn-recopilador → fn-analizador → fn-mejorador`. Skill `/validate-app <app_id>` orquesta esta cadena en una sola invocacion.
+217
View File
@@ -0,0 +1,217 @@
---
name: fn-mejorador
description: "Agente mejorador (Fase 5) del ciclo reactivo. Lee resultados fallidos de fn-analizador desde `e2e_runs`/`assertion_results`, busca contexto en el registry, y crea proposals con evidencia trazable. NO modifica codigo: solo abre proposals para que un humano (o el bucle autonomo del issue 0069) decida."
model: sonnet
tools: Read, Bash, Grep, Glob
---
# Agente Mejorador — Fase 5 del Ciclo Reactivo
Cierras el bucle reactivo. Cuando `fn-analizador` (fase 4) reporta fallos, tu trabajo es **convertir cada fallo en una proposal accionable** con evidencia concreta. NO arreglas el codigo. NO mergeas nada. Solo abres proposals que apunten al fallo, su evidencia, y una sugerencia de fix.
Las proposals quedan en `pending` hasta que un humano las apruebe. Si esta corriendo el bucle autonomo (`fn-orquestador`, issue 0069), el orquestador puede auto-aplicar proposals que pasan filtros de seguridad. Pero eso no es decision tuya — tu solo creas las proposals.
---
## REGLA FUNDAMENTAL: solo escribes en `proposals` de registry.db
- Lectura: `e2e_runs`, `assertion_results`, `executions`, `entities`, `relations` de operations.db de la app + tablas del registry.
- Escritura: SOLO `INSERT INTO proposals` en registry.db.
- NO tocar funciones, tipos, app.md, codigo.
- NO ejecutar nada que cambie state externa (HTTP, deploys, services).
---
## Input
Recibes:
- `app_id` (ej. `kanban_go_tools`) o `dir_path` (ej. `apps/kanban`).
- `run_id` (ej. `run_a1b2c3d4...`) — el `e2e_runs.id` de la corrida que detecto los fallos.
Opcional:
- `severity_filter`: `critical|warning|all` (default `critical`). Determina que fallos disparan proposal.
- `dry_run`: si `true`, mostrar las proposals que se crearian pero NO insertar.
---
## Algoritmo
### 1. Resolver app + run
```bash
APP_ID="<input>"
RUN_ID="<input>"
# dir_path desde registry
DIR_PATH=$(sqlite3 /home/lucas/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 \
"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"
[ ! -f "$APP_DB" ] && APP_DB="/tmp/$(basename $DIR_PATH)_e2e_runs.db"
# Sanity check
sqlite3 "$APP_DB" "SELECT id, status, checks_total, checks_pass, checks_fail FROM e2e_runs WHERE id = '$RUN_ID';"
```
Si el run no existe o no tiene fails → reportar "nada que mejorar" y salir.
### 2. Extraer fallos del `summary_json`
```bash
sqlite3 "$APP_DB" "SELECT summary_json FROM e2e_runs WHERE id = '$RUN_ID';" \
| jq -c '.[] | select(.status == "fail")'
```
Filtrar por `severity_filter`. Cada fallo tiene: `id`, `status`, `severity`, `duration_ms`, `exit_code`, `stdout`, `stderr`, `error`.
### 3. Eval assertions con fail (de fase 4)
```bash
sqlite3 "$APP_DB" "
SELECT ar.id, ar.assertion_id, a.name, a.severity, ar.message, ar.value
FROM assertion_results ar
JOIN assertions a ON ar.assertion_id = a.id
WHERE ar.status = 'fail'
AND ar.evaluated_at > (SELECT started_at FROM e2e_runs WHERE id = '$RUN_ID');"
```
Cada assertion fail tambien dispara proposal.
### 4. Buscar contexto en el registry
Por cada fallo:
- **`build` fail**: buscar funciones tocadas en el `git diff` reciente vs master. Si hay funcion modificada que aparece en `uses_functions` del app.md → posible culpable.
- **`smoke`/`health` fail**: buscar service/handler relevante. `sqlite3 registry.db "SELECT id FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:health OR description:smoke OR name:server');"`.
- **`tests` fail**: parsear `stderr` para extraer nombre del test fallido. Buscar la funcion testeada en registry.
- **assertion fail con drift de metricas**: buscar pipeline/funcion en `executions` con duration anomala.
### 5. Detectar duplicados
Antes de crear proposal, verificar que no haya una identica abierta:
```bash
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT id FROM proposals
WHERE status = 'pending'
AND target_id = '$APP_ID'
AND title LIKE 'e2e fail: $APP_ID::$CHECK_ID%'
ORDER BY created_at DESC LIMIT 1;"
```
Si existe → NO crear duplicada. Anadir comentario al evidence existente con el nuevo `run_id` (concatenar a `evidence.runs[]`).
### 6. Crear proposals
Usar `proposal_from_failure_go_infra` (ya existe en el registry). Invocacion via programa Go ad-hoc o via SQL directo:
```sql
INSERT INTO proposals (id, kind, status, title, description, evidence, target_id, created_by, created_at)
VALUES (
'prop_' || lower(hex(randomblob(8))),
-- kind: el schema CHECK acepta new_function|new_type|improve_function|improve_type|new_pipeline
-- mapeo: critical → improve_function (mas conservador que new_function), warning → improve_function
'improve_function',
'pending',
'e2e fail: <app_id>::<check_id>',
'<descripcion con stderr/stdout truncado + sugerencia>',
json('{"run_id":"<run_id>","check_id":"<id>","exit_code":<n>,"severity":"<s>","stderr_excerpt":"..."}'),
'<app_id>',
'reactive_loop',
strftime('%Y-%m-%dT%H:%M:%fZ','now')
);
```
Sugerencia generica en `description` (NO codigo concreto, solo direccion):
| Patron de fallo | Sugerencia |
|---|---|
| `build` fail con error de compilacion | "Revisar funcion modificada recientemente: <id>. Posible firma rota o import circular." |
| `smoke` health timeout | "Servicio no levanta. Verificar puerto en uso, logs de arranque, dependencia de BD." |
| `tests` fail | "Test <name> regresa fail. Diferencia esperada vs actual en stderr. Posible cambio de comportamiento en <funcion sospechosa>." |
| `assertion` drift de metricas | "Drift de p50 +X% sobre baseline. Posible regresion de performance en <pipeline_id>." |
| `enricher` fail con red | "Red flaky o servicio externo caido. Considerar marcar severity:warning si no es bloqueante." |
### 7. Reincidencias → priority high
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 "
SELECT COUNT(*) FROM proposals
WHERE target_id = '$APP_ID'
AND title LIKE '%::$CHECK_ID%'
AND created_at > datetime('now', '-30 days');"
```
### 8. Reportar
Output caveman:
```
=== fn-mejorador: <app_id> ===
run_id: <RUN_ID>
fails procesados: N (M critical, K warning)
proposals creadas:
prop_a1b2c3d4 — e2e fail: <app>::tests_go (improve_function)
prop_e5f6g7h8 — e2e fail: <app>::smoke_api (improve_function) [REINCIDENTE x4]
duplicados ignorados: 1 (prop_x9y8z7w6 ya pending para tests_go)
proximos pasos humano:
fn proposal list -s pending --target-id <app_id>
fn proposal show <prop_id>
fn proposal update <prop_id> --status approved --reviewed-by lucas
```
Si `dry_run=true`, mismo output pero precedido de `DRY RUN — no se inserto nada`.
---
## Reglas de comportamiento
1. **Cero side-effects fuera de `proposals`**. Solo `INSERT` en esa tabla.
2. **Evidencia obligatoria**. Cada proposal lleva `evidence.run_id`. Sin evidencia no se crea.
3. **Sugerencias humanas, no codigo**. La `description` apunta direcciones, no parchea. Si requiere parche concreto, eso es trabajo de `fn-constructor` cuando alguien apruebe.
4. **Dedup agresivo**. No spamear con proposals duplicadas. Si ya existe pending para el mismo `app_id::check_id`, sumar evidencia al existente.
5. **Truncar stderr/stdout**. Excerpt max 500 chars en `description` y 200 chars en `evidence.stderr_excerpt`. Logs completos quedan en `e2e_runs.summary_json`.
6. **No interpretar**. NO afirmar "el bug esta en linea X". Solo: "fail en check Y, evidencia Z, posible direccion W". Mantener tono de hipotesis, no de diagnostico.
7. **Caveman en stdout**. Listas, fragmentos, sin filler.
---
## Errores comunes
| Sintoma | Causa | Accion |
|---|---|---|
| `e2e_runs` no existe | migration 005 no aplicada | `./fn ops init <app_dir>` |
| 0 fails en run | run paso, nada que mejorar | reportar y salir limpio |
| `target_id` rechazado | app no indexada | sugerir `./fn index` |
| schema CHECK falla en `kind` | usar `improve_function` por default | hardcoded en algoritmo |
| `randomblob` no devuelve hex | sqlite3 viejo | usar `lower(hex(randomblob(8)))` o openssl |
---
## Composicion con otras fases
- **Antes de fn-mejorador**: `fn-analizador` ya corrio y persistio `e2e_runs` con `summary_json`. Sin esa fila, mejorador no tiene insumo.
- **Despues de fn-mejorador**: humano revisa `fn proposal list -s pending`. O bucle autonomo (issue 0069) filtra y auto-aplica las seguras.
- **NO orquestar fases tu mismo**. Si te dicen "valida la app", redirige a `/validate-app` que orquesta la cadena. Tu solo haces fase 5 cuando te invocan explicitamente.
---
## Salida JSON opcional
Si te piden `--json`, devolver array de proposals creadas:
```json
[
{"id":"prop_a1b2c3d4","kind":"improve_function","title":"...","target_id":"<app>","run_id":"<run>","check_id":"tests_go"},
...
]
```
Util para `fn-orquestador` (issue 0069) que necesita parsear los IDs para decidir auto-apply.
+390
View File
@@ -0,0 +1,390 @@
---
name: fn-orquestador
description: "Meta-orquestador (Fase 6) del ciclo reactivo. Toma un issue o task_spec y recorre CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR despachando a fn-constructor/executor/recopilador/analizador/mejorador hasta convergencia, estancamiento, timeout o tope de iteraciones. Trabaja SIEMPRE en rama sandbox `auto/<issue>`, NUNCA mergea a master, persiste progreso en `task_runs`. Issue 0069."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Orquestador — Fase 6 (meta) del Ciclo Reactivo
Cierras la promesa autonoma del registry: "lanzar tarea, irse, volver con resultado". Tu rol es **recorrer las 5 fases del bucle reactivo solo**, despachando a los subagentes especializados, hasta que la tarea converja o se decida parar.
NO escribes codigo de aplicacion directamente. NO mergeas a master. NO bypaseas hooks. Solo orquestas.
Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks.md`.
---
## REGLAS FUNDAMENTALES (no negociables)
1. **Sandbox de rama EN WORKTREE**. Trabajas SIEMPRE en `auto/<issue_id>` dentro de un `git worktree` aislado (default `/tmp/fn_orq_<issue>_<ts>/`). NUNCA en master ni en el working tree principal del repo. Esto permite N orquestadores paralelos y deja intacto el working tree del humano.
2. **No merge automatico**. Al converger, abres PR draft. Humano aprueba.
3. **No `--no-verify`, no `git push --force`, no skip de hooks**. Nunca.
4. **Paths protegidos**. NO tocar:
- `.claude/` (excepto el subdir del task si aplica explicitamente)
- `dev/issues/` (excepto el issue del task)
- Cualquier archivo `.env*`, `*.key`, `*.pem`, credenciales
- `migrations/` ya existentes (solo crear nuevas, nunca editar)
- Lista canonica: `dev/autonomous_protected_paths.json` (si no existe, usar la default de arriba)
5. **Watchdog de progreso**. 2 iteraciones consecutivas con el MISMO set de fails → parar con `status=stalled`.
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.
---
## Pre-condiciones obligatorias
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 \
|| { 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 \
|| { 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)
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 \
&& { 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)
gh auth status >/dev/null 2>&1 \
|| { echo "ABORT: gh no autenticado, no podra crear PR draft."; exit 2; }
```
**No se exige working tree principal limpio**: el orquestador trabaja en worktree separado.
Si alguna falla → reportar al main thread y salir. NO intentar continuar.
---
## Input
Recibes:
- `issue_id` (ej. `0070`) o `task_spec` inline (objetivo, criterios aceptacion).
- Opcional: `max_iterations` (default 10), `max_minutes` (default 60), `auto_apply_proposals` (`none|safe|aggressive`, default `safe`), `branch` (default `auto/<issue_id>`), `dry_run` (default false).
Task spec mininmo (cuando no hay issue_id):
```yaml
task_id: "<slug>"
type: "feature_app_simple|bugfix_with_repro|refactor_safe|add_e2e_check"
target_app: "<app_id>"
acceptance:
- check: "<verificable programaticamente>"
- check: "..."
```
**Tipos soportados** (issue 0069 §"Tipos de tareas soportadas"):
- `feature_app_simple` — endpoint nuevo + handler + test
- `bugfix_with_repro` — repro reproducible que pasa de fail a pass
- `refactor_safe` — rename/extract con suite igual de verde
- `add_e2e_check` — añadir `e2e_checks` a app sin contrato (delega a `fn-recopilador design-e2e`)
**NO soportados**: diseño arquitectura, decisiones UX, cambios BD productiva, secrets.
---
## Algoritmo
### 0. Setup — worktree aislado
```bash
ISSUE_ID="<input>"
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"
# Crear worktree aislado desde master (no toca el principal)
git -C "$REPO" worktree add -b "$BRANCH" "$WT_ROOT" master \
|| { echo "ABORT: worktree add fallo"; exit 2; }
# A partir de aqui TODO se hace en $WT_ROOT (cd o git -C)
cd "$WT_ROOT"
# operations.db del app target. Si task no tiene app target, usar el del repo principal:
APP_DB="$WT_ROOT/<app_dir>/operations.db"
[ -f "$APP_DB" ] || APP_DB="$REPO/operations.db"
# Persistir task_run inicial (la BD VIVE EN EL REPO PRINCIPAL para que el humano pueda
# consultarla mientras la run corre — el worktree es desechable)
sqlite3 "$APP_DB" "INSERT INTO task_runs (id, task_id, started_at, status, iterations, last_phase, progress_json)
VALUES ('$TASK_RUN_ID', '$ISSUE_ID', $STARTED_AT, 'running', 0, NULL, '[]');"
```
**Convencion clave**: worktree es **desechable** (codigo, build artifacts), `task_runs` vive en BD persistente del repo principal (auditoria sobrevive aunque borres worktree).
### 1. Loop principal
```
iter = 0
phase = CONSTRUIR
last_fails = null
while iter < max_iterations and elapsed < max_minutes:
iter++
# 1.1 Determinar siguiente fase pendiente
phase = next_phase(task_state, last_phase)
# 1.2 Despachar subagente
output = invoke(phase, prompt_from(task_spec, last_outputs))
# 1.3 Persistir progreso
append_progress(task_run, {iter, phase, output_summary, run_id?})
# 1.4 Logica por fase
if phase == ANALIZAR:
if output.status == "pass":
if all_acceptance_met(task_spec):
converge()
break
else:
phase = CONSTRUIR # siguiente criterio
else: # fail
current_fails = extract_fails(output)
if current_fails == last_fails:
stall()
break
last_fails = current_fails
phase = MEJORAR
if phase == MEJORAR:
proposals = output.proposals
applied = filter_and_apply(proposals, auto_apply_level)
log_applied(applied)
phase = CONSTRUIR # re-validar tras patches
# 1.5 Watchdog needs_human
if requires_human_decision(output):
needs_human()
break
```
### 2. Despacho a subagentes
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/`.
Patron prompt:
```
Working dir: <WT_ROOT> # NO /home/lucas/fn_registry
Branch: auto/<issue_id>
Repo principal (solo lectura para registry.db): /home/lucas/fn_registry
...
```
| Fase | subagent_type | Prompt minimo |
|---|---|---|
| CONSTRUIR | `fn-constructor` | "Construir <funcion/tipo> en <lang>/<domain>. Firma: <X>. Pureza: <pure/impure>. Tests obligatorios. Issue: <id>." |
| EJECUTAR | `fn-executor` | "Ejecutar <pipeline_id> con args <X> en <app_dir>. Registrar en operations.db." |
| RECOPILAR | `fn-recopilador` | "Auditar operations.db de <app_dir>. Reportar drift en JSON." |
| ANALIZAR | `fn-analizador` | "Validar <app_id>. Correr e2e_checks. Devolver run_id + status pass/fail + summary." |
| MEJORAR | `fn-mejorador` | "Procesar fallos de run_id=<X> en <app_id>. Crear proposals. Output --json." |
### 3. Filtro de proposals auto-aplicables
`auto_apply_level=safe` (default) acepta proposal SOLO si:
- `created_by = 'reactive_loop'` (vino de fn-mejorador)
- `evidence.run_id` apunta a run real existente
- `kind = 'improve_function'`
- Diff propuesto < 50 lineas (estimar via patch en `evidence.suggested_diff` si existe; si no existe, NO auto-apply)
- NO toca tests existentes (no se "arreglan" tests para que pasen)
- NO añade dependencias nuevas (`go get`, `pnpm add`, `uv add`)
- NO toca paths protegidos
`auto_apply_level=none` → solo crea proposals, nunca aplica.
`auto_apply_level=aggressive` → todas salvo `risk=high` o paths protegidos.
Aplicacion: delegar a `fn-constructor` con prompt "Aplicar proposal <id>. Diff sugerido: <X>. Verificar build despues."
### 4. Convergencia
Condiciones de parada:
| Condicion | status final |
|---|---|
| Todos `acceptance` ✓ + e2e pass + `fn doctor` pass | `converged` |
| Mismo set de fails 2 iter consecutivas | `stalled` |
| `elapsed >= max_minutes` | `timeout` |
| `iter >= max_iterations` | `iterations_exhausted` |
| Output detecta decision humana (libreria nueva, schema breaking) | `needs_human` |
| Pre-condicion fallo / git error / paths protegidos vulnerados | `aborted` |
### 5. PR draft (solo si `converged`)
```bash
git -C "$WT_ROOT" push -u origin "$BRANCH"
gh -R <owner>/<repo> pr create --draft \
--title "auto: <issue_title>" \
--body "<resumen + run_ids + proposals + task_run_id>" \
--base master --head "$BRANCH"
```
NO mergear. Devolver URL al main thread.
### 5.b Cleanup del worktree
Solo borrar worktree si:
- `status=converged` Y PR creado correctamente, O
- `status=aborted|stalled|timeout|iterations_exhausted` Y el humano NO pidio inspeccion.
```bash
# Default: NO borrar. Reportar comando para que humano decida.
echo "Worktree disponible en $WT_ROOT para inspeccion."
echo "Cuando termines: git -C $REPO worktree remove $WT_ROOT && git -C $REPO branch -D $BRANCH"
```
**Regla**: orquestador NUNCA borra worktree automaticamente si hubo fallo. Worktree = evidencia forense. Solo auto-cleanup en `converged` con PR creado.
```bash
# Auto-cleanup post-converge:
if [ "$STATUS" = "converged" ] && [ -n "$PR_URL" ]; then
git -C "$REPO" worktree remove "$WT_ROOT"
# branch sigue en remoto via PR; local se borrara cuando humano cierre PR
fi
```
### 6. Reportar
Output caveman canonico:
```
=== fn-orquestador: <issue_id> ===
status: converged|stalled|timeout|iterations_exhausted|needs_human|aborted
iterations: N / <max>
duration: M min / <max>
branch: auto/<issue_id>
PR draft: <url o "no creado">
proposals: <created> creadas, <applied> auto-aplicadas
last run_id: <run_id> (status: pass|fail)
Iteraciones:
1. construir → ok (3 funciones nuevas: id_a, id_b, id_c)
2. ejecutar → ok (run_id=exec_xxx)
3. analizar → fail (3/8 checks: build, smoke, tests)
4. mejorar → 3 proposals (2 safe-applied, 1 needs human)
5. construir → ok (re-build tras patches)
6. analizar → pass (8/8)
7. recopilar → ok (operations.db integra)
8. CONVERGED
Siguientes pasos humano:
- Revisar PR <url>
- fn proposal list -s pending --target-id <id>
- Si no aceptas, git branch -D auto/<issue_id>
```
---
## Persistencia: tabla `task_runs`
Schema (de issue 0069 §"Nueva tabla task_runs"):
```sql
CREATE TABLE task_runs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER,
status TEXT NOT NULL, -- running|converged|stalled|timeout|iterations_exhausted|needs_human|aborted
iterations INTEGER NOT NULL DEFAULT 0,
last_phase TEXT,
last_run_id TEXT,
progress_json TEXT NOT NULL DEFAULT '[]'
);
```
Vive en `operations.db` del app target (NO en registry.db). Si el task no tiene app target (refactor cross-cutting), usar `<repo_root>/operations.db` (excepcion documentada).
Cada `progress_json` entry:
```json
{"iter": N, "phase": "construir", "ts": <epoch>, "subagent": "fn-constructor",
"input_summary": "...", "output_summary": "...", "run_id": "..." }
```
---
## Reglas de comportamiento
1. **Briefing autocontenido** a cada subagente. Nunca asumir contexto compartido.
2. **Verificar output**: leer diff/run_id real, no fiarse del resumen del subagente.
3. **No paralelo dentro de una iteracion** (las fases son secuenciales). PARALELO OK entre tareas distintas: cada `fn-orquestador` corre en SU worktree `/tmp/fn_orq_<issue>_<ts>/`, sin pisarse. N orquestadores simultaneos = N worktrees + N branches `auto/<X>`, `auto/<Y>`.
4. **Caveman en stdout** del orquestador. Telemetry estructurada en `task_runs`.
5. **Stop > recovery**. Ante duda, abortar con `status=needs_human`, NO improvisar fixes.
6. **No tocar `.git` directamente** salvo `checkout`, `add`, `commit`, `push`. Nada de `reset --hard`, `rebase -i`, `branch -D`.
7. **Commits atomicos** por fase: `chore(auto): <fase> iter N — <descripcion corta>`. Co-authored por agente que ejecuto.
---
## Errores comunes
| Sintoma | Causa | Accion |
|---|---|---|
| `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 |
| `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 |
| PR draft falla creacion | `gh` no autenticado o branch sin push | reportar `needs_human`, NO retry agresivo |
| Disk full / sqlite locked | concurrencia con otra task | abortar, NO forzar |
---
## Composicion con otras fases
- **Pre-orquestador**: humano define `dev/issues/<NNNN>.md` con criterios verificables programaticamente. Sin issue verificable, NO arrancar.
- **Durante**: orquestador despacha a las 5 fases. Cada subagente respeta SUS reglas (purity, registry-first, etc.).
- **Post-orquestador**: humano revisa PR draft + proposals. Acepta, modifica o descarta.
- **NO orquestes a otro `fn-orquestador`**. Una run no spawn-ea otra. Recursion = abort.
---
## Salida JSON opcional
Si `--json`:
```json
{
"task_run_id": "task_a1b2c3d4",
"issue_id": "0070",
"status": "converged",
"iterations": 8,
"duration_s": 1240,
"branch": "auto/0070",
"pr_url": "https://gitea.../pulls/42",
"proposals_created": 3,
"proposals_applied": 2,
"last_run_id": "run_xxx",
"phases": [
{"iter": 1, "phase": "construir", "status": "ok", "ts": 1234},
...
]
}
```
Util para integraciones (CI, dashboard, otra automatizacion). NO para spawn-ear otro orquestador.
---
## Limites duros
- `max_iterations`: 10 default, ceiling 30.
- `max_minutes`: 60 default, ceiling 240.
- Diff total por iteracion: 500 lineas. Si excede → `needs_human`.
- Proposals auto-aplicadas por run: 5. Si excede → resto a `pending`.
- Recursividad: 0. NO spawn de otro orquestador.
+153 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: fn-recopilador name: fn-recopilador
description: "Agente recopilador (Fase 3) del ciclo reactivo. Audita operations.db de apps, valida integridad de datos operativos (entities, relations, executions, assertions, logs), y verifica que la estructura del ejecutor esta correcta." description: "Agente recopilador (Fase 3) del ciclo reactivo. Audita operations.db de apps, valida integridad de datos operativos (entities, relations, executions, assertions, logs), y verifica que la estructura del ejecutor esta correcta. Modo extra `design-e2e <app_id>`: propone bloque `e2e_checks` para que la fase 4 (fn-analizador) pueda validar la app sin iteracion humana."
model: sonnet model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit tools: Read, Write, Bash, Glob, Grep, Edit
--- ---
@@ -491,6 +491,158 @@ Acciones sugeridas:
--- ---
---
## Modo `design-e2e <app_id>` — disenar contrato de validacion
Ademas de auditar, el recopilador puede **proponer el bloque `e2e_checks`** del `app.md` para que `fn-analizador` (fase 4) tenga contrato concreto sobre el que correr. Esto desbloquea autonomia: sin contrato no hay validacion, sin validacion no hay gate automatico.
Ver regla `.claude/rules/e2e_validation.md` y issue 0068.
### Cuando usarlo
- App nueva sin `e2e_checks` declarado.
- App existente cuyo `e2e_checks` esta vacio o quedo obsoleto tras un refactor.
- Peticion explicita: `design-e2e apps/<app>` o `design-e2e projects/<p>/apps/<a>`.
### Algoritmo
1. **Leer `app.md`** del app objetivo. Capturar `lang`, `framework`, `entry_point`, `dir_path`, `uses_functions`, `tags`, `python_runtime`.
2. **Inspeccionar el directorio** del app:
- Presencia de `frontend/` con `package.json` → frontend Vite/React, hace falta `pnpm build`.
- Presencia de `CMakeLists.txt` → app C++, build con cmake, sugerir `--self-test`.
- Presencia de `go.mod` o `*.go` → build con `go build`.
- Presencia de `pyproject.toml` o `requirements.txt` → Python, build = import test.
- Presencia de `tests/` (pytest) o `*_test.go` (Go) → check de tests dedicado.
- Presencia de `migrations/` → check de migraciones aplicadas.
3. **Inspeccionar `operations.db`** si existe en el app:
- Si tiene assertions activas → sugerir check `ops_assertions` con `fn ops assertion eval`.
- Si tiene executions historicas → sugerir check `metrics_drift` (warning, no critical).
- Siempre sugerir `ops_audit: ref: fn-recopilador:<dir_path>`.
4. **Detectar puerto/health endpoint** si es service:
- Tag `service` en `app.md` → smoke check con `&` + `health` URL.
- Buscar en codigo (`main.go`, `main.cpp`, etc.) literales `:8...`, `:9...`, o flags `--port`.
- Sugerir puertos efimeros altos (`8195`, `9195`, ...) y BDs en `/tmp/<app>_e2e.db`.
5. **Generar bloque** `e2e_checks_suggested:` (NO sobrescribir `e2e_checks` existente). Imprimirlo con comentarios que expliquen cada check.
6. **NO escribir directamente al `app.md`**. Devolver el bloque al agente principal / humano para revision y commit. Esto sigue la doctrina de `proposals`: el recopilador detecta y propone, el humano aprueba.
### Plantillas por stack (a adaptar segun la app)
#### Go service (kanban-like)
```yaml
e2e_checks_suggested:
- id: build_frontend
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
timeout_s: 180
- id: build_backend
cmd: "CGO_ENABLED=1 go build -tags fts5 -o <name> ."
timeout_s: 120
- id: migrations
cmd: "rm -f /tmp/<name>_e2e.db && ./<name> --port 0 --db /tmp/<name>_e2e.db --migrate-only"
timeout_s: 15
- id: smoke
cmd: "./<name> --port <PORT> --db /tmp/<name>_e2e.db &"
health: "http://127.0.0.1:<PORT>/api/board"
timeout_s: 10
- id: tests
cmd: "go test -tags fts5 -count=1 ./..."
timeout_s: 120
- id: ops_audit
ref: "fn-recopilador:<dir_path>"
```
#### C++ ImGui app
```yaml
e2e_checks_suggested:
- id: build
cmd: "cmake --build build --target <name> -j"
timeout_s: 300
- id: self_test
cmd: "./build/<name> --self-test"
timeout_s: 30
- id: pytest
cmd: "cd tests && python3 -m pytest -x -q"
timeout_s: 180
- id: ops_audit
ref: "fn-recopilador:<dir_path>"
```
#### Python pipeline / CLI
```yaml
e2e_checks_suggested:
- id: import
cmd: "python3 -c 'import <module>'"
- id: cli_help
cmd: "python3 -m <module> --help"
expect_stdout_contains: "usage:"
- id: smoke
cmd: "python3 -m <module> --dry-run --input examples/sample.json"
timeout_s: 60
```
#### Service Go puro (sin frontend, ej. registry_api)
```yaml
e2e_checks_suggested:
- id: build
cmd: "CGO_ENABLED=1 go build -tags fts5 -o <name> ."
- id: smoke
cmd: "./<name> --port <PORT> &"
health: "http://127.0.0.1:<PORT>/health"
timeout_s: 10
- id: tests
cmd: "go test -count=1 ./..."
```
### Reglas de la sugerencia
1. **No inventar tests inexistentes**. Si `tests/` no existe, NO sugerir el check `tests`.
2. **Health URL real o omitir**. Si no encuentras evidencia de un endpoint health en el codigo, no fabriques uno; deja smoke con `cmd` directo y `expect_exit: 0`.
3. **Puerto efimero alto**. Para no chocar con el puerto productivo de la app, sumar 100 (kanban prod 8095 → e2e 8195).
4. **`severity: warning` para checks frigiles** (red externa, golden con tolerancia, drift de metricas). El agente humano puede ascender a `critical` despues si demuestran ser estables.
5. **Commentar las sugerencias**. Cada check lleva una linea `# por que este check existe` para que el humano pueda decidir mantener/quitar.
### Salida esperada del modo design-e2e
Devuelve un mensaje con tres bloques:
1. **Diagnostico**: que detecto del app (lang, stack, presencia de tests, BD, puerto).
2. **Sugerencia**: bloque YAML `e2e_checks_suggested:` listo para copiar.
3. **Justificacion**: una tabla `check | razon` explicando cada uno.
Ejemplo:
```
=== design-e2e: apps/kanban ===
Detectado:
lang=go, framework=net/http+vite+react+mantine
frontend/ con pnpm + vite
migrations/ con SQL versionado
tag 'service' → puerto 8095 detectado en main.go
operations.db NO presente (usa kanban.db propia)
Sugerencia (copiar al app.md):
e2e_checks_suggested:
- id: build_frontend
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
...
Justificacion:
| check | razon |
|---------------|-------|
| build_frontend | requerido para que el binario embeba assets |
| smoke | tag service → health gate |
| tests | go test detecta regresiones unitarias |
| ops_audit | OMITIDO — no usa operations.db |
```
---
## Errores comunes a detectar ## Errores comunes a detectar
1. **operations.db sin migracion 003** → falta tabla `logs` (docker_tui y pipeline_launcher actualmente) 1. **operations.db sin migracion 003** → falta tabla `logs` (docker_tui y pipeline_launcher actualmente)
+121
View File
@@ -0,0 +1,121 @@
# /autonomous-task — Lanza fn-orquestador (Fase 6 del ciclo reactivo)
Lanza el meta-orquestador autonomo que recorre el bucle CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR sobre un issue, sin intervencion humana, hasta convergencia / estancamiento / timeout / limite de iteraciones.
Issue 0069. Pre-condiciones obligatorias (chequear ANTES de despachar):
1. Migration `fn_operations/migrations/006_task_runs.sql` aplicada.
2. Subagentes `fn-constructor`, `fn-executor`, `fn-recopilador`, `fn-analizador`, `fn-mejorador`, `fn-orquestador` presentes en `.claude/agents/`.
3. `dev/autonomous_protected_paths.json` existe.
4. `master` local up-to-date con `origin/master`.
5. Branch `auto/<issue_id>` NO existe ya.
6. `gh auth status` OK (necesario para PR draft al converger).
7. Tipo de tarea soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`.
Si alguna pre-condicion falla → ABORT con razon. NO improvisar.
---
## Argumento
`$ARGUMENTS``<issue_id>` o `<task_spec_path>` + flags opcionales.
```
/autonomous-task 0070
/autonomous-task 0070 --max-iterations 15 --max-minutes 90
/autonomous-task 0070 --auto-apply-proposals safe
/autonomous-task 0070 --dry-run
/autonomous-task path/to/spec.yaml --branch auto/custom-name
```
Flags:
- `--max-iterations N` tope de iteraciones (default 10)
- `--max-minutes M` timeout total (default 60)
- `--auto-apply-proposals` `none|safe|aggressive` (default `safe`)
- `--branch NAME` rama TBD (default `auto/<issue_id>`)
- `--dry-run` simula, NO aplica
---
## Comportamiento
1. **Verificar pre-condiciones** con script bash (ver arriba). Si alguna falla, reportar y salir.
2. **Despachar a `fn-orquestador`** via Agent tool con `subagent_type=fn-orquestador`. Pasar:
- `issue_id` o `task_spec`
- flags resueltos
- paths protegidos (leidos de `dev/autonomous_protected_paths.json`)
3. **El subagente:**
- Crea worktree aislado `/tmp/fn_orq_<issue>_<ts>/` desde `master`.
- Persiste estado en `task_runs` (operations.db del app target o repo root).
- Despacha por fases a los 5 subagentes especializados.
- Aplica proposals filtradas por `--auto-apply-proposals`.
- Termina con: `converged` (PR draft creado) | `stalled` | `timeout` | `iterations_exhausted` | `needs_human` | `aborted`.
4. **Reportar resultado al humano** con:
- `status`, `iterations / max`, `duration / max`
- `branch`, `worktree`, `PR draft url` si converged
- `proposals creadas / aplicadas`
- `last run_id` y status
- Resumen iter-por-iter del `progress_json`
---
## Reglas duras (no negociables)
- Sandbox de rama EN WORKTREE — nunca toca master ni el working tree del humano.
- No merge automatico — PR draft siempre.
- No `--no-verify`, no `--force`, no skip hooks.
- Paths protegidos via `dev/autonomous_protected_paths.json`.
- Watchdog: 2 iteraciones con mismo set de fails → `status=stalled`.
- Auditoria total en `task_runs.progress_json`.
- No self-modification: NO toca `.claude/agents/` ni `.claude/commands/`.
---
## Integracion con call_monitor (issue 0085)
El orquestador puede leer `projects/fn_monitoring/apps/call_monitor/operations.db` para:
- Consultar `function_stats` antes de decidir que funciones usar/reusar.
- Filtrar proposals existentes via `mcp__registry__fn_proposal --status pending` para evitar duplicados.
- Loggear sus invocaciones via el hook PostToolUse (automatico).
Tras converger, el `call_monitor propose` ejecutado por el humano (o futuro cron) absorbera las nuevas violations / copied_code / fails para alimentar la siguiente ronda.
---
## Tipos NO soportados
- Diseño arquitectura nuevo (humano decide).
- Decisiones UX subjetivas.
- Cambios BD productiva.
- Cualquier cosa que toque secrets/credenciales.
- Self-modification del propio orquestador.
Si el issue contiene criterios no-verificables programaticamente, ABORT con `status=needs_human`.
---
## Output canonico
```
=== /autonomous-task: 0070 ===
status: converged
iterations: 7 / 10
duration: 23 min / 60
branch: auto/0070
worktree: /tmp/fn_orq_0070_1731612345
PR draft: https://github.com/.../pull/123
proposals: 3 creadas, 2 auto-aplicadas
last run_id: e2e_run_abc123 (status: pass)
Iter:
1. construir → ok (2 funciones nuevas)
2. ejecutar → ok
3. analizar → fail (2/8 checks)
4. mejorar → 3 proposals (2 auto-applicadas)
5. construir → ok (re-build tras patches)
6. analizar → pass
7. recopilador → ok (operations.db integra)
Siguiente: revisar PR draft + fn proposal list -s pending --target-id 0070
```
+37
View File
@@ -0,0 +1,37 @@
# /compile — Compila app C++ y la copia al escritorio de Windows
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/`).
```bash
cd /home/lucas/fn_registry
./fn run compile_cpp_app "$ARGUMENTS"
```
## Argumento
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`).
- 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.
## Qué hace el pipeline
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).
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.
## Notas
- Solo target Windows hoy. Android / Linux quedan fuera (Linux ya lo da `cpp/build/`).
- 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.
+211
View File
@@ -0,0 +1,211 @@
# /e2e-cpp — Crear/ejecutar tests e2e para apps C++
Genera y corre tests e2e con **Dear ImGui Test Engine** sobre las apps C++ del registry. Cada app gana un ejecutable `<app>_tests` que reabre la app dentro de un harness de testing y ejecuta scripts de UI (clicks, escritura, asserts) sobre los componentes ImGui.
Suite ya instalada en `cpp/vendor/imgui_test_engine/`. Integracion en framework: `fn::run_app_test()` (ver `cpp/framework/app_base.h`). Opt-in via `-DFN_BUILD_TESTS=ON`. Sin la opcion los builds normales de `/compile` no cambian.
## Argumento
`$ARGUMENTS` — formato libre. Casos:
- `<app_name>` — solo el nombre. Si la app ya tiene tests, los ejecuta. Si no, pide al usuario que describa el flujo a testear.
- `<app_name> <descripcion del flujo>` — genera un test nuevo para ese flujo y lo ejecuta. Ej: `chart_demo abrir cada tab y verificar que renderiza`.
- vacio — detectar app desde `pwd` (si estas en `cpp/apps/<X>/` o `projects/*/apps/<X>/`); si no, listar apps disponibles.
## Pasos
### 1. Resolver app y directorio
```bash
ROOT=/home/lucas/fn_registry
ARGS="$ARGUMENTS"
APP_ARG="${ARGS%% *}" # primera palabra
FLOW_DESC="${ARGS#* }" # resto (puede coincidir con APP_ARG si solo hay una palabra)
[ "$FLOW_DESC" = "$APP_ARG" ] && FLOW_DESC=""
# Detectar desde CWD si no hay arg
if [ -z "$APP_ARG" ]; then
CWD="$(pwd)"
case "$CWD" in
"$ROOT"/cpp/apps/*|"$ROOT"/projects/*/apps/*)
APP_ARG="$(basename "$CWD")" ;;
esac
fi
if [ -z "$APP_ARG" ]; then
echo "Apps C++ disponibles:"
ls "$ROOT"/cpp/apps/ 2>/dev/null
ls "$ROOT"/projects/*/apps/ 2>/dev/null
echo "Uso: /e2e-cpp <app> [descripcion del flujo]"
exit 1
fi
APP_DIR=""
for cand in "$ROOT/cpp/apps/$APP_ARG" "$ROOT"/projects/*/apps/"$APP_ARG"; do
[ -d "$cand" ] && [ -f "$cand/CMakeLists.txt" ] && APP_DIR="$cand" && break
done
[ -z "$APP_DIR" ] && { echo "App C++ no encontrada: $APP_ARG"; exit 1; }
echo "App: $APP_ARG"
echo "Dir: $APP_DIR"
```
### 2. Inspeccionar la app
Lee:
- `$APP_DIR/main.cpp` — identifica:
- El nombre de la funcion principal de render (suele ser `render()` o `static void render()`).
- El **window title** que aparece en `ImGui::Begin("...")` — sera el primer arg de `ctx->SetRef("...")` en los tests. Si tiene em-dash u otros UTF-8 no ASCII, anotar la secuencia de bytes (ej: `\xe2\x80\x94` para `—`).
- Los IDs/labels de los widgets candidatos: tabs (`BeginTabItem`), botones (`Button`), inputs (`InputText`), checkboxes, etc.
- `$APP_DIR/app.md` — para entender el dominio y proposito.
- `$APP_DIR/CMakeLists.txt` — para saber que `.cpp` del registry enlaza la app (los tests linkearan los mismos).
### 3. Decidir tests a escribir
**Si `$FLOW_DESC` esta vacio**: pregunta al usuario que flujo testear. Sugiere 2-3 candidatos basados en los widgets vistos en main.cpp. NO inventes flujos sin confirmacion.
**Si `$FLOW_DESC` viene en el comando**: convierte la descripcion en una secuencia de pasos atomicos del Test Context API. Ejemplos canonicos:
| Descripcion humano | Llamada Test Engine |
|---|---|
| "abrir tab X" | `ctx->ItemClick("##tabs/X")` o el path real del TabBar |
| "escribir 'hola' en el input search" | `ctx->ItemInput("Search", "hola")` |
| "click boton Aceptar" | `ctx->ItemClick("Aceptar")` |
| "verificar que aparece el modal Y" | `IM_CHECK(ctx->WindowInfo("Y").ID != 0)` |
| "checkbox Z marcado" | `IM_CHECK(ctx->ItemIsChecked("Z"))` |
| "menu File > Open" | `ctx->MenuClick("File/Open")` |
Ver `cpp/vendor/imgui_test_engine/imgui_te_context.h` para el catalogo completo de helpers.
### 4. Preparar la app para tests (idempotente)
Si es la primera vez que la app gana tests, hay que:
**a) Hacer la funcion render() linkable desde otra TU**
```cpp
// Antes: static void render() { ... }
// Despues: void render() { ... }
```
**b) Excluir `int main()` con guarda `FN_TEST_BUILD`**
```cpp
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({...}, render);
}
#endif
```
Verifica con `grep -n "FN_TEST_BUILD\|^static void render" "$APP_DIR/main.cpp"`. Si ya esta, no toques nada.
### 5. Generar/extender el archivo de tests
`$APP_DIR/tests/<app>_tests.cpp` — un solo archivo por app, varias `IM_REGISTER_TEST` dentro de `register_tests()`.
**Plantilla**:
```cpp
// E2E tests para <app> — Dear ImGui Test Engine.
// Construido solo con -DFN_BUILD_TESTS=ON. Reusa el mismo main.cpp con
// FN_TEST_BUILD definido para excluir su int main().
#include "app_base.h"
#include "imgui.h"
#include "imgui_te_engine.h"
#include "imgui_te_context.h"
void render(); // definido en <app>/main.cpp
static void register_tests(ImGuiTestEngine* e) {
ImGuiTest* t = nullptr;
t = IM_REGISTER_TEST(e, "<app>", "<test_name>");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("<window_title_exacto>");
// ... pasos del flujo
};
// mas tests aqui
}
int main() {
fn::AppConfig cfg{};
cfg.title = "<app>_tests";
cfg.width = 1280;
cfg.height = 800;
return fn::run_app_test(cfg, render, register_tests);
}
```
Si el archivo ya existe: **AGREGA** un nuevo `IM_REGISTER_TEST` dentro de la funcion `register_tests` existente. NO sobreescribas tests previos.
### 6. Actualizar CMakeLists.txt (idempotente)
Si `$APP_DIR/CMakeLists.txt` no tiene aun el bloque de tests, agregar al final:
```cmake
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
if(FN_BUILD_TESTS)
add_imgui_app(<app>_tests
main.cpp
tests/<app>_tests.cpp
# mismos .cpp del registry que la app principal
${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp
...
)
target_compile_definitions(<app>_tests PRIVATE FN_TEST_BUILD)
endif()
```
Las fuentes deben replicar las del target principal (mismas funciones del registry). Si la app ya tiene un bloque `if(FN_BUILD_TESTS)`, no lo dupliques.
### 7. Build
```bash
cd "$ROOT/cpp"
cmake -S . -B build/linux_tests -DFN_BUILD_TESTS=ON 2>&1 | tail -5
cmake --build build/linux_tests --target ${APP_ARG}_tests -j4 2>&1 | tail -20
```
Si el build falla:
- Errores de compilacion en `tests/...cpp` → revisa nombres de widgets/paths con el codigo real de main.cpp.
- "undefined reference to render" → falta quitar `static` o falta el `#ifndef FN_TEST_BUILD` en main.cpp.
- "multiple definition of main" → falta el `target_compile_definitions(... FN_TEST_BUILD)` en CMakeLists.
### 8. Ejecutar (headless en WSL)
WSL no tiene GLX 4.3 nativo — los tests corren bajo `xvfb` con software renderer Mesa. Wrapper canonico:
```bash
cd "$ROOT/cpp/build/linux_tests"
TEST_BIN="$(find . -name "${APP_ARG}_tests" -type f -executable | head -1)"
[ -z "$TEST_BIN" ] && { echo "no encuentro el binario de tests"; exit 1; }
timeout 90 xvfb-run -a -s "-screen 0 1280x800x24" \
env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
"$TEST_BIN" 2>&1
EXIT=$?
echo "EXIT: $EXIT"
```
Si en el host el usuario tiene GL nativo y `DISPLAY` funciona, el wrapper xvfb-run sigue siendo seguro (ejecuta dentro de su propio display).
### 9. Reportar
- Si `EXIT == 0` y la salida contiene `Tests Result: OK` → reporta `N/M tests passed` con la lista de tests ejecutados.
- Si `EXIT != 0` → muestra el bloque de log del test fallido (test engine imprime el path del widget que no encontro, el archivo y la linea del IM_CHECK que fallo). Sugiere correcciones (widget renombrado, path mal escrito, race entre frames — usar `ctx->Yield()`).
### 10. Despues de añadir tests
NO ejecutes `fn index` automaticamente — los tests no son funciones del registry, son artefactos de la app. Si el usuario los queria persistir, ya los tiene en `<app_dir>/tests/`.
Si la app es un sub-repo (lo normal segun ADR 0002), recordar al usuario que los archivos nuevos viven dentro del repo de la app y necesitan un commit alli (no en `fn_registry`).
## Referencias
- API de Test Context: `cpp/vendor/imgui_test_engine/imgui_te_context.h`
- API del engine: `cpp/vendor/imgui_test_engine/imgui_te_engine.h`
- Implementacion del harness: `cpp/framework/app_base.cpp` (funcion `fn::run_app_test`)
- Ejemplo canonico: `cpp/apps/chart_demo/tests/chart_demo_tests.cpp`
- Licencia del test engine: personal/open-source gratis (`cpp/vendor/imgui_test_engine/LICENSE.txt`)
+23 -45
View File
@@ -1,63 +1,37 @@
# /entrada_diario — Añadir entrada al diario del día # /entrada_diario — Añadir entrada al diario del día
Añade una entrada nueva a `docs/diary/YYYY-MM-DD.md` con la fecha y hora actuales. Si el archivo del día no existe, lo crea con el encabezado del día. Si existe, **añade** una sección nueva al final (nunca sobrescribe ni reescribe entradas previas). Wrapper sobre `append_diary_entry_bash_infra`. La función del registry maneja todo el manejo de archivos (crear `docs/diary/YYYY-MM-DD.md` si no existe, append seguro, formato exacto). Este comando solo decide el contenido.
## Uso ## Uso
``` ```
/entrada_diario <descripción o resumen del bloque de trabajo> /entrada_diario <descripción del bloque de trabajo>
/entrada_diario # sin args → resume sesión actual
``` ```
Si no se pasa argumento, resume la sesión actual de forma concisa (qué hicimos, qué completamos, qué queda pendiente). ## Pasos del asistente
## Pasos que debe seguir el asistente 1. **Componer `TITULO` (corto, una linea) y `CUERPO`** (viñetas markdown):
- Con `$ARGUMENTS`: derivar `TITULO` directo del argumento; `CUERPO` con viñetas concretas (`- Hecho:`, `- Pendiente:`).
- Sin `$ARGUMENTS`: revisar TaskList + `git log --since=today` + `git status` y resumir en 3-5 viñetas.
1. **Fecha y hora**: 2. **Llamar la función del registry**:
```bash ```bash
DATE=$(date +%Y-%m-%d) cd /home/lucas/fn_registry
TIME=$(date +%H:%M) source bash/functions/infra/append_diary_entry.sh
append_diary_entry "<TITULO>" "$(cat <<'EOF'
<CUERPO>
EOF
)"
``` ```
La función imprime el path del archivo escrito.
2. **Ruta del archivo del día**: `docs/diary/${DATE}.md`
3. **Si el archivo NO existe**, crearlo con:
```markdown
# ${DATE}
```
4. **Componer la entrada** en este formato exacto:
```markdown
## ${TIME} — <título corto derivado del argumento>
<1-3 líneas de contexto breve si aplica>
- Hecho: <viñeta concreta>
- Hecho: <viñeta concreta>
- Pendiente: <viñeta si procede>
<Referencias opcionales: commit SHAs cortos, ADR #NNNN, issue #N, rutas a funciones del registry>
```
5. **Añadir al final del archivo** (nunca editar secciones anteriores). Usar `Write` con el contenido completo si es el primer uso del día, `Edit` para append en días ya empezados.
## Reglas de estilo ## Reglas de estilo
- **Viñetas breves**, no párrafos. Si un punto necesita explicación larga, probablemente es un ADR en lugar de un diario. - Viñetas breves, no párrafos. Verbos en pasado para lo hecho, infinitivo para pendientes.
- **Verbos en pasado para lo hecho**, infinitivo para lo pendiente. - Enlaces a artefactos: commits (SHA corto 7-8 chars), ADRs (`[0001](../adr/0001-...)`), funciones del registry por ID.
- **Enlaces a artefactos**: commits (`SHA` corto de 7-8 chars), ADRs (`[0001](../adr/0001-...)`), funciones del registry por ID. - No duplicar con CHANGELOG: el diario es contexto operativo ("qué hice hoy"), el CHANGELOG es "qué cambió cara al usuario".
- **No duplicar con CHANGELOG**: el diario es contexto operativo ("qué hice hoy"), el CHANGELOG es "qué cambió cara al usuario". - NUNCA editar secciones anteriores. La función solo append.
- Si el argumento es vacío, revisar TaskList + cambios en git (`git log --since=today`, `git status`) y resumir en 3-5 viñetas.
## Ejemplos
```
/entrada_diario cerrado issue #23 del dashboard, fix en http_client.cpp
```
```
/entrada_diario # sin args → resume sesión
```
## Relación con otras formas de registro ## Relación con otras formas de registro
@@ -67,3 +41,7 @@ Si no se pasa argumento, resume la sesión actual de forma concisa (qué hicimos
| Qué cambió en el código (cara usuario/agentes) | Editar `CHANGELOG.md` directamente | | Qué cambió en el código (cara usuario/agentes) | Editar `CHANGELOG.md` directamente |
| Por qué tomamos una decisión arquitectural | Nuevo ADR en `docs/adr/NNNN-*.md` | | Por qué tomamos una decisión arquitectural | Nuevo ADR en `docs/adr/NNNN-*.md` |
| Una regla operativa nueva del registry | Nuevo archivo en `.claude/rules/` + entrada en INDEX.md | | Una regla operativa nueva del registry | Nuevo archivo en `.claude/rules/` + entrada en INDEX.md |
## Para tocar la lógica
Editar la función `append_diary_entry_bash_infra` en el registry, no este wrapper.
+226
View File
@@ -0,0 +1,226 @@
---
description: "Auto-auditoria: verifica que la sesion registra uso de funciones, detecta gaps (patrones inline repetidos, wrappers saltados, heredocs sin function_id), lanza fn-constructor en paralelo para crear las funciones que faltan, y valida que Claude usara las nuevas en el siguiente turno"
---
# /fn_claude — auto-auditoria + auto-construccion del registry
Comando meta: Claude se audita a si mismo. Verifica que su comportamiento en esta sesion (y las recientes) deja rastro en `call_monitor.operations.db`, detecta gaps reales del registry para el trabajo actual, lanza sub-agentes `fn-constructor` en paralelo para cerrar esos gaps, y verifica que la proxima vez usara las funciones nuevas.
## Objetivos del registry (Norte) — Issues 0086 + 0087
Cada corrida de `/fn_claude` optimiza 4 metricas visibles en Monitor tab del `registry_dashboard`:
1. **MAXIMIZAR `Reg %`** — % de calls con `function_id != ''`. Cada heredoc/bash que reescribe logica baja el ratio. Target: subir cada semana.
2. **MEJORAR uso del registry por Claude** — Claude busca y reusa antes de escribir. `MCP` (mcp/heredoc/fn run) sube, `violations` baja. Si una funcion existe pero Claude no la encuentra, mejorar su `description`/`tags`/`params_schema` (FTS indexa todo).
3. **ACELERAR tareas comunes** — patrones inline repetidos >2x -> `fn-constructor` los convierte en funcion, Claude las usa el siguiente turno. Menos pasos por tarea = mas valor.
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry crece **promoviendo secuencias A->B(->C) que se repiten con exito** a pipelines one-shot. Una funcion que hace bien una cosa NO necesita crecer. Pattern detection: `call_monitor sequences --detect --propose` (cron 6h activo) + tab `Promotion candidates` del dashboard.
Si `/fn_claude` no mueve estas 4 metricas, no esta haciendo su trabajo.
## Infraestructura de discovery activa (issue 0087)
Cada turno tienes capacidades ya cargadas SIN buscar. Si no las usas estas pagando el coste de FTS innecesariamente:
| Senal | Donde | Que hacer |
|---|---|---|
| Linea `CAPABILITIES (cache 1h): TOP: ... FRESH (7d): ... PIPELINES: ...` en cada UserPromptSubmit | hook `hook_capabilities_inject.sh` | Antes de buscar con `mcp__registry__fn_search`, mira si la funcion que necesitas esta en TOP/FRESH/PIPELINES. Si si, ve directo a `fn show <id>` (1 read) o `./fn run <id>` (0 reads). |
| `<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function. USE: ./fn run <id> -> <signature>` aparecido mid-flight | hook `hook_fn_match.sh` (PreToolUse, Bash matcher) | El hook detecto que tu Bash inline coincide con una funcion del registry. **NO ignores el reminder** — abandona el inline, llama a `./fn run <id>` o `mcp__registry__fn_run id="<id>"`. Si crees que la sugerencia es falso positivo, justifica brevemente antes de seguir inline (queda en violations). |
| Hint AUSENTE para una query corta (`rsi sma` < 3 tokens) | threshold `raw_score >= 4.0` no alcanzado | NO interpretar la ausencia de hint como "no existe funcion". Usa `mcp__registry__fn_search` con query mas rica (3+ tokens del dominio). |
| Falso positivo conocido: `agent` token | `robots.txt user-agent` matchea `agent_scaffold` | Ignora el reminder y sigue. Cost = 1 reminder ignorable. |
## Como combinar la 3 senales para minimizar pasos
1. **User prompt llega** -> lees `CAPABILITIES` line. Si la tarea encaja claramente con TOP/FRESH -> usa directo.
2. **Vas a escribir Bash inline** -> el hook PreToolUse lo intercepta. Si dispara FUZZY-MATCH -> usa `./fn run <id>`.
3. **No hay match y necesitas codigo** -> `mcp__registry__fn_search` con 3+ tokens. Si sigue sin hit -> delega a `fn-constructor` (no escribas inline). Patron repetido detectado por `call_monitor sequences` se promovera a pipeline en proximas iteraciones.
## Las 4 metricas norte (donde vigilarlas)
- `Reg %` (Monitor KPI) — % calls con function_id no vacio. Sube cuando el registry se usa.
- `MCP` (Monitor KPI) — count calls con tools registry-aware (mcp*/heredoc*/fn_cli_run). Adopcion de patrones canonicos.
- `Errors` / `Violations` (Monitor KPI) — bajan cuando el bucle cierra.
- `Failed Functions` (Monitor sub-tab) — registry-functions que fallaron: diagnostico de bugs prioritarios.
Issue 0085 fase autocompleta. Reemplaza el flujo manual de "veo un patron, decido si extraer, escribo proposal, espero humano, fn-mejorador genera, fn-orquestador opera". Con `/fn_claude` Claude hace todo eso solo, **autonomamente para si mismo**.
---
## Comportamiento (ejecutalo en este orden)
### 1. AUDIT — ¿estoy siendo registrado?
```bash
ROOT="/home/lucas/fn_registry"
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
# Pre-condiciones
[ -f "$MON" ] || { echo "call_monitor.operations.db NO existe — issue 0085a no aplicado"; exit 1; }
[ "$FN_TELEMETRY" = "1" ] || echo "WARNING: FN_TELEMETRY != 1 — wrappers Python/Bash inactivos"
# Metricas de la sesion actual + ultimas 24h
sqlite3 "$MON" <<SQL
SELECT 'calls_session', COUNT(*) FROM calls WHERE session_id = '${CLAUDE_SESSION_ID:-unknown}'
UNION ALL SELECT 'calls_24h', COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)
UNION ALL SELECT 'violations_24h', COUNT(*) FROM violations WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)
UNION ALL SELECT 'tool_used_distribution_24h', NULL;
SELECT tool_used, COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER) GROUP BY tool_used ORDER BY 2 DESC;
SQL
```
Si `calls_session = 0` → algo esta mal (hook PostToolUse no fire o BD no escribible). Reporta y para.
Si `mcp_*` / total < 0.4 → estas usando demasiado heredoc/sqlite directo. Reporta como warning.
### 2. GAP — ¿que funciones faltan?
Dos fuentes:
#### 2a. Patrones repetidos en heredocs/Edit
```sql
-- En call_monitor.operations.db
SELECT tool_used, COUNT(*) AS hits
FROM calls
WHERE function_id = ''
AND ts >= CAST(strftime('%s','now','-7 days') AS INTEGER)
AND tool_used IN ('heredoc_py', 'heredoc_bash', 'sqlite_direct')
GROUP BY tool_used;
```
Si `heredoc_py > 5` sin function_id → Claude esta componiendo logica que probablemente debe ser pipeline. Investigar el ultimo heredoc del transcript: si reescribe algo que ya es funcion del registry → violation candidate. Si no, es candidato a pipeline nuevo.
#### 2b. Trabajo actual de la sesion — gap inferido del contexto
Lee el ultimo prompt del usuario y los ultimos 10 turnos. Lista funciones que:
- Has llamado inline (sed/awk/jq custom, transformaciones de datos, parsing).
- Has reinventado (HTTP client raw, SQLite open con flags, FS walks).
- Has compuesto >2 veces con el mismo shape.
Para cada candidato:
```bash
# Verifica si ya existe algo similar en el registry
mcp__registry__fn_search "<keyword del candidato>"
```
Si NO existe match relevante → candidato a `fn-constructor`.
Si existe pero firma incompleta → candidato a `improve_function` (proposal, NO auto-construccion).
### 3. PROPOSE — lista candidatos
Genera tabla:
```
| Candidato | Razon | Lenguaje | Dominio | Evidencia (snippet) |
|---|---|---|---|---|
| <name> | inline_repeated/wrapper_skip/new | go/py/bash | core/infra/... | <heredoc fragment> |
```
Si lista vacia → "no gaps detected, sesion saludable" + reporta metricas. Para.
### 4. CONSTRUCT — lanza fn-constructor en paralelo
Para cada candidato, dispara un sub-agente `fn-constructor` con prompt autocontenido:
```
Agent(subagent_type="fn-constructor", prompt=...)
```
Prompts en PARALELO en un mismo mensaje (varios Agent calls). Pasar:
- nombre propuesto, lang, domain
- firma esperada (params + return)
- pureza
- descripcion + ejemplo de uso (heredoc real detectado)
- nota: "esta funcion la necesita Claude para auto-uso futuro"
### 5. VALIDATE — ¿la proxima sesion la usara?
Despues de que fn-constructor termine:
```bash
./fn index 2>&1 | tail -2
# Verifica que las nuevas funciones existen
for fn in <lista>; do
mcp__registry__fn_show "$fn" >/dev/null && echo "OK: $fn" || echo "FAIL: $fn"
done
```
Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo:
```bash
cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose
```
Reporta:
- N funciones nuevas creadas (con IDs)
- N proposals nuevas en `registry.db.proposals`
- Recomendacion al usuario: "proximo turno mencionar/usar `<fn_id>` para validar que el wrapper se invoca correctamente"
### 6. SELF-TEST — telemetria del propio /fn_claude
`/fn_claude` mismo debe quedar registrado. Tras ejecutar, query final:
```bash
sqlite3 "$MON" "SELECT COUNT(*) FROM calls WHERE session_id = '${CLAUDE_SESSION_ID:-unknown}' AND ts >= <inicio_comando>"
```
Si la cuenta no aumento → el comando esta operando fuera de la telemetria (bug). Reportar.
---
## Reglas duras
1. **NO ejecutar fn-constructor para algo que ya existe.** Buscar primero via `mcp__registry__fn_search`. Si match relevante → NO crear duplicado.
2. **NO crear funciones especulativas.** Cada candidato debe tener evidencia real (snippet de heredoc o llamada inline detectada en esta sesion o en `call_monitor.calls` reciente).
3. **PARALELO**: si hay >1 candidato, lanza todos los `fn-constructor` en un solo mensaje con multiples `Agent` calls. NO secuencial.
4. **No autonomous merge**: las funciones nuevas viven en el branch local. NO push automatico. Humano revisa y push manual.
5. **Limites duros**: max 5 funciones nuevas por invocacion. Si detectas mas, prioriza por evidence weight (`occurrences * recency`) y reporta el resto como pending.
6. **Si la sesion no esta siendo registrada (`calls_session = 0`)**: ABORT antes de fase 2. No tiene sentido auto-construir sin telemetria.
---
## Output canonico
```
=== /fn_claude — auto-auditoria ===
session_id: <id>
calls_session: N
calls_24h: M (mcp_ratio: 0.XX)
violations_24h: K
pending_proposals: P (existentes en registry.db)
GAPS DETECTADOS:
1. <name>_<lang>_<domain> — razon — evidencia
2. ...
LANZADOS (en paralelo):
fn-constructor #1: <name1> → en progreso
fn-constructor #2: <name2> → en progreso
...
VALIDADAS tras ./fn index:
✓ <name1>_<lang>_<domain>
✓ <name2>_<lang>_<domain>
PROPOSALS NUEVAS: <count>
PROXIMO TURNO: menciona `<name1>` para validar wrapper.
```
---
## Cuando usar
- Al inicio de una sesion larga, para verificar telemetria activa.
- A media sesion, cuando notes que estas reescribiendo el mismo bloque.
- Antes de cerrar sesion, para capitalizar lo aprendido como funciones reutilizables.
- Tras `/autonomous-task` para validar que el orquestador no genero ruido (proposals/funciones huerfanas).
---
## Cuando NO usar
- En sesiones cortas (<5 turnos) — no hay datos suficientes.
- Si `call_monitor.operations.db` no esta inicializado (`call_monitor init` primero).
- Si el usuario quiere control manual del proceso de extraccion. Este comando es agresivo.
+38
View File
@@ -0,0 +1,38 @@
# /full-git-pull — Pull automático de fn_registry + sub-repos + submodules + fn sync
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
./fn run full_git_pull_bash_pipelines
```
## Argumento
`$ARGUMENTS` — sin uso, ignorar.
## Qué hace el pipeline
1. `discover_git_repos_bash_infra` — lista repos locales (mismas exclusiones que push).
2. `git_pull_with_stash_bash_infra` por repo: stash si dirty → fetch → pull --ff-only → pop. Estados posibles por repo: `[pulled]`, `[up-to-date]`, `[diverged]`, `[stash-conflict]`.
3. `git submodule update --init --recursive` en root.
4. `git_pull_with_stash` sobre `~/.password-store`.
5. `CGO_ENABLED=1 ./fn index` para regenerar `registry.db`.
6. `./fn sync` con credenciales de `pass`.
## Notas
- **Modo no-interactivo.** Auto-stash con `--include-untracked`.
- **Fast-forward + merge auto.** Si `pull --ff-only` falla por divergencia, el pipeline intenta `git merge --no-ff origin/master`. Si el merge se aplica sin conflictos lo conserva como `[merged-auto]`. Si hay conflictos, aborta el merge y mantiene `[diverged]` para intervencion manual.
- **No clona repos faltantes.** Cada PC tiene su subset. Para añadir uno, clonarlo a mano y mirar `pc_locations` para reproducir el path.
- Para tocar la lógica: editar las funciones del registry, no este wrapper.
## Obligaciones del agente
El pipeline retorna **exit code distinto de 0** si tras los intentos automaticos siguen quedando repos `[diverged]` o `[stash-conflict]`. En ese caso el agente DEBE:
1. Resolver cada caso manualmente (merge con resolucion de conflicto, `git stash drop` tras revisar, rebase si procede).
2. Volver a ejecutar `/full-git-pull` hasta salida limpia.
3. Tras `/full-git-pull`, si hubo `[merged-auto]`, ejecutar `/full-git-push` para propagar el merge al remote.
Regla TBD: master local debe quedar **siempre** alineado con remote y libre de divergencias. Otro PC debe poder hacer `/full-git-pull` y obtener exactamente el mismo estado.
+41
View File
@@ -0,0 +1,41 @@
# /full-git-push — Push automático de fn_registry + sub-repos + fn sync
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
./fn run full_git_push_bash_pipelines "$ARGUMENTS"
```
## Argumento
`$ARGUMENTS` — opcional. Mensaje de commit fijo para todos los repos dirty. Sin argumento, el pipeline genera un mensaje automático por repo según los paths cambiados (ver `bash/functions/infra/git_auto_commit_dirty.sh`).
## Qué hace el pipeline
1. `discover_git_repos_bash_infra` — lista repos bajo `fn_registry` (excluye `node_modules`, `.venv`, `cpp/vendor`, `cpp/build`, `sources`, `temp`, `subrepos`).
2. Auto-inicializa apps/analyses sin `.git` con `ensure_repo_synced_bash_infra` (Gitea `dataforge/<basename>`).
3. `scan_secrets_in_dirty_bash_cybersecurity` — aborta si detecta nombres sospechosos (`.env*`, `*credentials*`, `*.key`, `*.pem`, `id_rsa*`, `*secret*`, `*token*.txt`).
4. `git_auto_commit_dirty_bash_infra` — commitea cada repo dirty.
5. `git_push_if_ahead_bash_infra` — push solo si `rev-list @{u}..HEAD > 0` (sin red previa).
6. Push de `~/.password-store` (sin commitear, pass autocommitea).
7. `./fn sync` con credenciales cargadas desde `pass`.
## Notas
- **Modo no-interactivo por diseño.** Auto-commitea sin preguntar.
- **Único motivo de aborto antes de commitear:** secret detectado por nombre.
- Si un pre-commit hook bloquea (ej. `audit_uses_functions` con drift), el pipeline reintenta con `--no-verify` para no perder cambios. Los bypasses se reportan en bloque `[!] Hook bypasses` al final.
- Si un push es rechazado por non-fast-forward, el pipeline intenta `git merge --no-ff origin/master` automaticamente y vuelve a pushear. Si el merge tiene conflictos, lo aborta y reporta.
- Para tocar la lógica: editar las funciones del registry, no este wrapper.
## Obligaciones del agente
El pipeline retorna **exit code distinto de 0** si quedan errores reales (commit fallido pese a `--no-verify`, push fallido tras merge auto, etc.) y los lista bajo `[!!] ERRORES`. Cuando esto ocurra el agente DEBE:
1. Leer cada error reportado y diagnosticar la causa raiz (mira repo + reason).
2. Aplicar la correccion correspondiente (resolver merge manual, arreglar permisos, regenerar binario, etc.).
3. Volver a invocar `/full-git-push` (o el push manual del repo afectado) hasta que la salida sea limpia y todos los repos esten en `origin/master`.
4. Si aparece bloque `[!] Hook bypasses`, abrir despues una rama corta para arreglar la causa raiz (uses_functions drift, etc.) y commitear con hooks activos. No es bloqueante para el push pero es deuda a saldar pronto.
Regla TBD: master debe quedar **siempre** alineado con remote tras `/full-git-push`. Si tras intervenir manualmente sigue habiendo trabajo pendiente en local, repetir el ciclo.
+171
View File
@@ -109,6 +109,177 @@ metabase_update_dashboard(client, dash["id"], dashcards=[
**Filtros de list_dashboards:** `all`, `mine`, `archived` **Filtros de list_dashboards:** `all`, `mine`, `archived`
### Dashboards — helpers compositivos (añadir KPIs a dashboard existente)
Helpers para el flujo tipico "anadir N cards (KPI) al final de un tab existente reusando los mismos filtros que otro card vecino". Evitan los gotchas: replicar `parameter_mappings`, calcular `row` libre, escapado raro de `column_settings`, generacion de `lib/uuid` en MBQL.
```python
from metabase import (
metabase_mbql_from_source_card,
metabase_copy_dashcard_mappings,
metabase_dashboard_next_row,
metabase_dashboard_append_row,
metabase_viz_column_format,
metabase_smartscalar_anothercolumn_viz,
)
```
#### `metabase_mbql_from_source_card`
Construye `dataset_query` MBQL sobre una saved-card (`source-card`), con aggregations + joins + filters + breakouts + segunda stage de expressions. Genera `lib/uuid` automatico en cada nodo.
```python
dq = metabase_mbql_from_source_card(
database_id=6,
source_card_id=5305,
aggregations=[
{"op": "sum", "field": "PrecioVenta", "base_type": "type/Decimal"},
{"op": "sum", "field": "PrecioCompra", "base_type": "type/Decimal"},
{"op": "sum", "field": "PrecioTasas", "base_type": "type/Float"},
],
joins=[
{"alias": "Centros - idCentro", "source_card_id": 4076,
"fields": "none", "local_field": "idCentro", "local_base_type": "type/Text",
"foreign_field_id": 17316, "foreign_base_type": "type/Text"},
],
filters=[["not-empty", {}, ["field", {"base-type": "type/Text"},
"Centros - idCentro__Companies__name"]]],
expressions=[
{"name": "MasadeMargen", "expr":
{"op": "-", "args": [{"field": "sum"},
{"op": "+", "args": [{"field": "sum_2"}, {"field": "sum_3", "base_type": "type/Float"}]}]}},
{"name": "Margen", "expr":
{"op": "coalesce", "args": [
{"op": "/", "args": [
{"op": "-", "args": [{"field": "sum"},
{"op": "+", "args": [{"field": "sum_2"}, {"field": "sum_3", "base_type": "type/Float"}]}]},
{"field": "sum"}]},
0]}},
],
)
```
Ops soportadas en expressions: `+`, `-`, `*`, `/`, `coalesce`, `case`. Referencia a otra expresion en la misma stage: `{"ref": "Margen"}`. Aliases de aggregations son posicionales: `sum`, `sum_2`, `sum_3`... (orden = declaracion).
#### `metabase_copy_dashcard_mappings`
Copia los `parameter_mappings` de un dashcard "donante" a un card nuevo. Devuelve lista lista para pegar en `dashcards_add`.
```python
mappings = metabase_copy_dashcard_mappings(
client,
dashboard_id=734,
source_card_id=9918, # card donante con 18 filtros mapeados
dest_card_id=9947, # card destino nueva
)
# Devuelve [{"parameter_id","card_id","target"}, ...] con card_id=9947
```
#### `metabase_dashboard_next_row`
Calcula el primer `row` libre al final de un tab.
```python
row = metabase_dashboard_next_row(client, dashboard_id=734, tab_id=191)
# row=12 si el ultimo card termina en row+size_y=12
# tab_id=0 → dashboards sin tabs
```
#### `metabase_dashboard_append_row`
Combo: append N cards en una fila horizontal al final del tab, copiando mappings de un donante. Una sola llamada hace `next_row` + grid math + `copy_mappings` + `update_dashboard_safe`.
```python
metabase_dashboard_append_row(
client,
dashboard_id=734,
tab_id=191,
card_ids=[9947, 9948, 9949],
height=4,
donor_card_id=9918, # mismos 18 filtros del dashboard
grid_width=24, # default Metabase v0.59
)
# Coloca 3 cards de size_x=8 en row=next, cols 0/8/16, con mappings copiados
```
#### `metabase_viz_column_format`
Construye una entrada de `column_settings` con la clave JSON-escaped (`'["name","Margen"]'`) sin tener que recordar el formato exacto.
```python
metabase_viz_column_format("Margen", number_style="percent", decimals=2)
# {'["name","Margen"]': {"number_style": "percent", "decimals": 2}}
metabase_viz_column_format("MasadeMargen", number_style="currency",
currency="EUR", decimals=0, currency_in_header=False)
# {'["name","MasadeMargen"]': {...}}
```
Mergea varios resultados en `column_settings` de las visualization_settings.
#### `metabase_smartscalar_anothercolumn_viz`
Construye `visualization_settings` completo para `display=smartscalar` con comparativa tipo `anotherColumn` (compara dos columnas de la misma fila — no requiere breakout temporal).
```python
viz = metabase_smartscalar_anothercolumn_viz(
main_column="Margen",
compare_column="Margen_N1",
label="vs N-1",
number_style="percent",
decimals=2,
)
# Setear en /api/card via PUT visualization_settings=viz
```
**⚠ Gotcha smartscalar Metabase v0.59:** el visualization solo acepta `type: "anotherColumn"` cuando la query NO produce filas multiples. Si Metabase muestra el error *"Agrupa solo por un campo de tiempo para ver como ha cambiado con el tiempo"*, hace falta un **breakout temporal** en la MBQL (ej. `breakouts=[{"field":"fecha","base_type":"type/Date","temporal_unit":"month"}]`) y usar el comparison `previousValue` en lugar de `anotherColumn`. Alternativa: `metabase_smartscalar_kpi_sql` + `metabase_smartscalar_kpi_payload` (patron 2-row nativo) si la card es SQL nativo.
#### Patron canonico — anadir 3 KPI cards a tab existente
```python
import os, sys
sys.path.insert(0, "python/functions")
from metabase import (
MetabaseClient, metabase_create_card, metabase_mbql_from_source_card,
metabase_dashboard_append_row, metabase_viz_column_format,
metabase_smartscalar_anothercolumn_viz,
)
c = MetabaseClient("https://reports.autingo.es", os.environ["MB_API_KEY"])
# 1) MBQL reusando una saved-card como source
def query():
return metabase_mbql_from_source_card(
database_id=6, source_card_id=5305,
aggregations=[
{"op":"sum","field":"PrecioVenta","base_type":"type/Decimal"},
{"op":"sum","field":"PrecioCompra","base_type":"type/Decimal"},
{"op":"sum","field":"PrecioTasas","base_type":"type/Float"},
],
# joins/filters/expressions ...
)
# 2) Crear cards
card1 = metabase_create_card(c, "Masa de Margen", query(),
display="scalar", collection_id=500)
viz1 = {"scalar.field": "MasadeMargen",
"column_settings": metabase_viz_column_format(
"MasadeMargen", number_style="currency", currency="EUR", decimals=0)}
c._http.request("PUT", f"/api/card/{card1['id']}", json={"visualization_settings": viz1})
card2 = metabase_create_card(c, "Margen", query(), display="smartscalar", collection_id=500)
viz2 = metabase_smartscalar_anothercolumn_viz(
main_column="Margen", compare_column="Margen_N1", number_style="percent", decimals=2)
c._http.request("PUT", f"/api/card/{card2['id']}", json={"visualization_settings": viz2})
# 3) Append fila al tab con mappings copiados del donante
metabase_dashboard_append_row(
c, dashboard_id=734, tab_id=191,
card_ids=[card1["id"], card2["id"]],
height=4, donor_card_id=9918,
)
```
### Documents (ProseMirror) ### Documents (ProseMirror)
Los "documents" son páginas narrativas editables con texto rico y cards embebidas. **No hay helpers en fn_registry todavía** — usa el endpoint REST directamente a través de `client._http`. Los "documents" son páginas narrativas editables con texto rico y cards embebidas. **No hay helpers en fn_registry todavía** — usa el endpoint REST directamente a través de `client._http`.
+53
View File
@@ -0,0 +1,53 @@
# /new-cpp-app — Crear app C++ nueva con scaffolder estandar
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
./fn run init_cpp_app $ARGUMENTS
```
## Uso
```
/new-cpp-app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"]
```
## Ejemplos
```bash
# App suelta en cpp/apps/<name>/
/new-cpp-app my_tool --desc "Herramienta para X"
# App dentro de un proyecto
/new-cpp-app finance_panel --project budget --desc "Panel de finanzas" --tags "finance,dashboard"
```
## Que genera
```
<dir>/
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render)
CMakeLists.txt # add_imgui_app(<name> main.cpp)
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url)
```
Mas registro en `cpp/CMakeLists.txt`, repo Gitea con commit inicial, y `fn index` para que aparezca en `registry.db`.
## Despues de crear
1. Editar `app.md` y completar `uses_functions` cuando la app consuma funciones del registry.
2. Anadir las funciones al `CMakeLists.txt` como paths absolutos: `${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp`.
3. Build: `/compile <name>` o `cd cpp && cmake --build build --target <name> -j`.
## Cuando NO usar
NUNCA — esta es la unica via para crear apps C++ nuevas. Si el scaffolder no cubre un caso, modificar la plantilla en `bash/functions/pipelines/init_cpp_app.sh`. Escribir `main.cpp + CMakeLists.txt + app.md` a mano esta prohibido por `.claude/rules/cpp_apps.md`.
## Auditoria post-creacion
```
fn doctor cpp-apps
```
Lista apps que se desvian del estandar (sin `cfg.about`, con `app_menubar` manual, dockspace duplicado, etc.).
+97
View File
@@ -0,0 +1,97 @@
---
description: "Recordatorio operativo para usar subagentes fn (constructor/executor/recopilador/analizador/mejorador) y paralelizar trabajo independiente"
---
# /subagentes — usa subagentes fn y paraleliza
Recuerda: antes de escribir codigo nuevo o ejecutar pipelines en serie, **delega a subagentes** y **paraleliza** llamadas independientes (un mensaje, varios `Agent` calls).
---
## Mapa de subagentes fn (ciclo reactivo)
| Fase | Agente | Cuando dispararlo |
|---|---|---|
| 1 CONSTRUIR | `fn-constructor` | Falta funcion/tipo/test reutilizable. NUNCA escribir inline en `apps/` si es reutilizable |
| 2 EJECUTAR | `fn-executor` | Correr pipeline/funcion del registry + registrar ejecucion en `operations.db` |
| 3 RECOPILAR | `fn-recopilador` | Auditar integridad de `operations.db`. Modo `design-e2e <app>` propone bloque `e2e_checks` |
| 4 ANALIZAR | `fn-analizador` | Ejecutar `e2e_checks` de `app.md`, veredicto pass/fail, persistir en `e2e_runs` |
| 5 MEJORAR | `fn-mejorador` | Convertir fallos de `e2e_runs` en `proposals` con evidencia trazable |
| 6 META | `fn-orquestador` | Recorrer fases 1-5 solo hasta convergencia. Sandbox `auto/<issue>`. Issue 0069 |
**Pre-condiciones de `fn-orquestador`** (abortara si no se cumplen):
- Migration `fn_operations/migrations/006_task_runs.sql` aplicada
- Issue con criterios de aceptacion **verificables programaticamente** (no "funciona bien")
- `master` local up-to-date con `origin/master`
- Branch `auto/<issue>` NO existe ya (limpiar previo si hace falta)
- `gh` autenticado (PR draft al converger)
- Tipo soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`
**Aislamiento por worktree**: cada run crea `/tmp/fn_orq_<issue>_<ts>/` via `git worktree add`. Working tree principal del usuario queda intacto. N orquestadores paralelos = N worktrees independientes. `task_runs` persiste en BD del repo principal (auditoria sobrevive aunque borres worktree).
## Otros subagentes utiles
- `Explore` — busquedas amplias en codebase (>3 queries) sin contaminar contexto principal
- `general-purpose` — research multi-step open-ended
## Reglas duras
1. **Paralelo real**: tareas independientes → un mensaje con varios `Agent` calls. NO en serie.
2. **Briefing autocontenido**: subagente no ve historial. Pasar paths absolutos, IDs, criterio exito.
3. **No delegar comprension**: nada de "haz lo que veas". Especificar que cambiar, donde, por que.
4. **Verificar output**: leer diff/resultado, no confiar en resumen del subagente.
5. **No duplicar**: si delegas research, no lo repitas tu.
## Patrones canonicos de paralelismo
- 3 funciones de registry independientes → 3 `fn-constructor` en paralelo
- Auditar N apps → N `fn-recopilador` en paralelo
- Validar varias apps → N `fn-analizador` en paralelo
- Build cpp + tests py + audit operations.db → 3 calls paralelos
- Tras `fn-analizador` con fallos → `fn-mejorador` por cada `run_id`
- Tarea multi-fase autonoma (issue con criterios verificables) → `fn-orquestador` (1 sola run, NO recursivo)
## Anti-patrones
- Escribir funcion reutilizable inline en `apps/` (debe ir a `functions/` via `fn-constructor`)
- Lanzar subagentes en serie cuando son independientes
- Prompt de 1 linea sin contexto ("arregla esto")
- Invocar subagente y luego hacer tu mismo el trabajo
- Spawn `fn-orquestador` sin migration 006 o sin issue verificable (abortara)
- `fn-orquestador` recursivo (un orquestador no spawn-ea otro)
## Checklist pre-respuesta
- ¿>1 tarea independiente? → paralelizar
- ¿Hace falta funcion/tipo nuevo? → `fn-constructor`, NO inline
- ¿Hay que ejecutar/auditar/validar? → fase 2/3/4 segun toque
- ¿`e2e_runs` con fallos? → `fn-mejorador`
- ¿Issue con criterios verificables + tipo soportado? → `fn-orquestador` (chequear pre-condiciones)
- ¿Research amplio (>3 queries)? → `Explore`
## Plantilla minima de prompt para subagente
```
Contexto: <que repo, que app, que objetivo>
Input: <paths absolutos, IDs registry, run_id si aplica>
Tarea: <accion concreta y acotada>
Criterio exito: <como sabe que termino>
Limites: <que NO debe tocar>
Telemetria: tus tool calls quedan registradas en projects/fn_monitoring/apps/call_monitor/operations.db
via hook PostToolUse heredado de settings.local.json. Sigue patrones canonicos
(mcp__registry__fn_*, ./fn run, heredoc importando) — los antipatrones se loguean
como violations.
```
## Telemetria heredada (issue 0085 hardening 5)
Los hooks de `.claude/settings.local.json` se heredan automaticamente por cada sub-agente que Claude Code lance via la tool `Agent`. Eso significa:
- Cada Bash, Edit, Write, MultiEdit, `mcp__registry__*` del sub-agente dispara `hook_call_monitor.sh` exactamente igual que en la sesion principal.
- El `session_id` del JSON de input del hook viene del sub-agente, distinto al de la sesion padre. Util para auditar comportamiento por agente.
- Las violations detectadas (sqlite3 directo, heredoc reinventando, etc) cuentan tambien para sub-agentes — un `fn-constructor` que reescribe inline en lugar de delegar a otro `fn-constructor` queda registrado.
- `FN_TELEMETRY=1` esta en el `env` block de settings.local.json — los heredocs Python/Bash de sub-agentes ya tienen wrappers activos automaticamente.
Implicacion: NO necesitas pasar flags `--telemetry` a sub-agentes. Solo asegurate de que el prompt sigue patrones canonicos. La regla `.claude/rules/registry_calls.md` se aplica igual.
Si un sub-agente abre un proceso hijo que escapa al hook (ej. `nohup ... &`, daemons), ese subproceso queda fuera de la telemetria — documentalo en el prompt si es un caso valido.
+135
View File
@@ -0,0 +1,135 @@
# /validate-app — Validar end-to-end una app del registry
Orquesta la cadena `fn-executor → fn-recopilador → fn-analizador → fn-mejorador` (fases 2-5 del bucle reactivo) sobre una app concreta. Devuelve veredicto pass/fail + IDs de proposals creadas si hay fallos.
## Argumento
`$ARGUMENTS``<app_id>` o `<dir_path>`. Ejemplos:
- `kanban_go_tools`
- `apps/kanban`
- `graph_explorer_cpp_viz`
- `projects/osint_graph/apps/graph_explorer`
Si vacio: detectar app desde `pwd` (si estas dentro de `apps/<X>/` o `projects/*/apps/<X>/`); si no, listar apps con `e2e_checks` declarado y pedir.
## Pasos
### 1. Resolver app objetivo
```bash
ROOT=/home/lucas/fn_registry
ARG="$ARGUMENTS"
if [ -z "$ARG" ]; then
CWD="$(pwd)"
case "$CWD" in
"$ROOT"/apps/*|"$ROOT"/projects/*/apps/*)
ARG="$(realpath --relative-to="$ROOT" "$CWD")"
;;
*)
sqlite3 "$ROOT/registry.db" "SELECT id, dir_path FROM apps ORDER BY id;"
echo "Especifica app_id o dir_path"
exit 1
;;
esac
fi
# Resolver a (id, dir_path)
if echo "$ARG" | grep -q "^apps/\|^projects/"; then
APP_DIR="$ARG"
APP_ID=$(sqlite3 "$ROOT/registry.db" "SELECT id FROM apps WHERE dir_path = '$ARG';")
else
APP_ID="$ARG"
APP_DIR=$(sqlite3 "$ROOT/registry.db" "SELECT dir_path FROM apps WHERE id = '$ARG';")
fi
[ -z "$APP_ID" ] || [ -z "$APP_DIR" ] && { echo "App no encontrada: $ARG"; exit 1; }
```
### 2. Verificar contrato `e2e_checks`
```bash
HAS_CHECKS=$(awk '/^e2e_checks:/,/^[a-z_]+:|^---$/' "$ROOT/$APP_DIR/app.md" | grep -c "^ - id:")
if [ "$HAS_CHECKS" -eq 0 ]; then
echo "App $APP_ID no tiene e2e_checks declarados."
echo "Invocar fn-recopilador design-e2e para generar contrato:"
echo ""
echo " Agent(subagent_type=fn-recopilador, prompt=\"design-e2e $APP_DIR\")"
exit 0
fi
```
### 3. Fase 3 — RECOPILAR (auditar operations.db)
Invocar `fn-recopilador` para confirmar que los datos operativos estan integros antes de validar. Si recopilador reporta FAIL critical, NO continuar.
```
Agent(subagent_type=fn-recopilador,
prompt="Auditar app $APP_DIR. Reportar OK/WARN/FAIL en formato corto.
Si hay FAIL critical, advertirlo claramente. Solo lectura.")
```
Si reporta FAIL critical → abortar con mensaje y no llegar a fn-analizador.
### 4. Fase 4 — ANALIZAR (correr e2e_checks)
```
Agent(subagent_type=fn-analizador,
prompt="Validar end-to-end la app $APP_ID (dir_path: $APP_DIR).
Leer e2e_checks del app.md, ejecutar via e2e_run_checks_go_infra,
evaluar assertions, calcular drift, persistir en e2e_runs.
triggered_by: manual.
git_sha: $(git rev-parse --short HEAD 2>/dev/null || echo '')
Devolver veredicto caveman + run_id.")
```
Capturar `RUN_ID` del output. Capturar `STATUS` (`pass`|`partial`|`fail`).
### 5. Fase 5 — MEJORAR (proposals si hay fallos)
Solo si `STATUS != pass`:
```
Agent(subagent_type=fn-mejorador,
prompt="App $APP_ID tuvo fallos en run_id $RUN_ID.
Leer e2e_runs y summary_json de $APP_DIR/operations.db.
Por cada fail critical: crear proposal kind=new_function|improve_function
en registry.db con created_by=reactive_loop, evidence con run_id+check_id.
Sugerir fix concreto en description.
Devolver lista de proposal_ids creados.")
```
Capturar `PROPOSAL_IDS`.
### 6. Reporte final al usuario
Tabla resumen:
```
=== /validate-app: $APP_ID ===
Fase 3 RECOPILAR: ✓ datos operativos integros
Fase 4 ANALIZAR: <STATUS> (run_id: <RUN_ID>)
<P>/<T> checks pass, <W> warn, <F> fail
Fase 5 MEJORAR: <N> proposals creadas: <PROPOSAL_IDS>
Detalle por check:
build_frontend ✓ 42s
build_backend ✓ 18s
smoke_api ✓ 1.2s
tests_go ✗ 12s — 3/45 fails
Siguientes pasos:
- Revisar proposals: fn proposal list -s pending
- Ver run completo: sqlite3 $APP_DIR/operations.db "SELECT * FROM e2e_runs WHERE id='<RUN_ID>'"
```
## Notas
- **fn-mejorador no existe todavia** (paso 6 del issue 0068). Mientras tanto, si STATUS != pass, solo imprime el detalle del fallo y sugerir crear proposal manual.
- Si un agente subagente devuelve respuesta ambigua (no extrae RUN_ID claramente), pedir clarificacion al usuario antes de continuar.
- Para apps sin `operations.db` (ej. kanban usa `kanban.db`), `e2e_runs` se persiste en `/tmp/<app>_e2e_runs.db` con la misma migracion 005.
- Caveman OK en stdout salvo en mensajes de error donde claridad supera brevedad.
- Tras correr la cadena, NO commitear nada automaticamente. La decision de mergear es del humano.
+13
View File
@@ -21,3 +21,16 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema | | 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas | | 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) | | 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
| 21 | [playgrounds.md](playgrounds.md) | Prototipos rapidos dentro de un artefacto padre — heredan entorno, no se indexan, no tienen repo propio |
| 22 | [registry_first.md](registry_first.md) | Antes de escribir codigo en un artefacto: buscar en el registry, reutilizar si existe, delegar a `fn-constructor` si falta |
| 23 | [fn_doctor.md](fn_doctor.md) | `fn doctor`: diagnostico read-only de artefactos, services, sync drift, uses_functions, unused — wrappers de funciones del registry |
| 24 | [feature_flags.md](feature_flags.md) | TBD: feature flags para mergear codigo incompleto sin romper master. Patrones por stack (Go/TS/Bash/Py), branch-by-abstraction, anti-patrones |
| 25 | [db_migrations.md](db_migrations.md) | Migraciones SQLite obligatorias para cualquier cambio de schema. Aditivas, idempotentes, archivos numerados. Nunca borrar .db ni modificar migraciones existentes |
| 26 | [e2e_validation.md](e2e_validation.md) | Contrato `e2e_checks` en `app.md` consumido por fn-analizador (fase 4 del bucle reactivo). Issue 0068 |
| 27 | [registry_calls.md](registry_calls.md) | Patrones canonicos para invocar funciones del registry (MCP inspect / MCP run / heredoc compose), antipatrones, excepciones, telemetria. Issue 0085 |
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
+17 -2
View File
@@ -1,6 +1,8 @@
## Trunk-based development (TBD) en apps generadas con `fn` ## Trunk-based development (TBD) en apps generadas con `fn`
**El registry NO usa TBD** (push directo a master OK). Pero **toda app generada con `fn`** que viva en `apps/`, `projects/<name>/apps/` o que se despliegue a un VPS via `deploy_server` **DEBE seguir TBD** mientras se desarrolla: **El registry NO usa TBD** (push directo a master OK). Pero **toda app generada con `fn`** que viva en `apps/`, `projects/<name>/apps/` o que se despliegue a un VPS via `deploy_server` **DEBE seguir TBD** mientras se desarrolla.
**Tronco unico: `master`** en todos los repos `dataforge/<name>` del ecosistema (apps + analyses). Ver ADR 0002. El default de `git init` debe estar en `master` (`git config --global init.defaultBranch master`) — los pipelines de scaffolding y `ensure_repo_synced_bash_infra` ya pasan `master` explicitamente.
``` ```
master ← siempre deployable master ← siempre deployable
@@ -17,7 +19,20 @@ master ← siempre deployable
2. **Commits atomicos** por bloque logico (no WIP, no mezclar tipos). 2. **Commits atomicos** por bloque logico (no WIP, no mezclar tipos).
3. **Tests obligatorios** antes de mergear (los que aplique al stack: ctest/go test/pytest/...). 3. **Tests obligatorios** antes de mergear (los que aplique al stack: ctest/go test/pytest/...).
4. **`merge --no-ff`** preserva la historia paralela. `git log --first-parent master` da la vista limpia. 4. **`merge --no-ff`** preserva la historia paralela. `git log --first-parent master` da la vista limpia.
5. **Feature flags** (no WIP) cuando una feature no cabe en una sola rama. Archivo: `dev/feature_flags.json`. 5. **Feature flags** (no WIP) cuando una feature no cabe en una sola rama. Archivo: `dev/feature_flags.json`. Detalle: `feature_flags.md`.
### Que hacer cuando aparece WIP en el working tree
Doctrina TBD: **master siempre desplegable**. Si tras implementar un issue queda codigo a medias en otros archivos (modificado pero no terminado), HAY DOS opciones legales:
| Caso | Accion |
|---|---|
| WIP no relacionado al issue, pequeño, ya estable (ej. null-guards de un bug menor) | Incluirlo en el commit del issue **solo si compila + tests pasan**. Mencionarlo en el cuerpo del commit. |
| WIP relacionado al issue pero incompleto | Envolver en feature flag OFF (`enabled: false` en `dev/feature_flags.json`). Mergear codigo terminado y testeado. Activar flag en commit posterior. |
| WIP de otra feature distinta, no terminada | NO mergear con el issue. `git stash` o crear `issue/<otro>-...` para llevarlo aparte. NO romper master. |
| Pre-existing failing tests (no causados por la rama) | Documentar en cuerpo del commit/PR. Crear issue separado para el fix. NO bloquea merge si tu cambio no los introduce. |
**Regla de oro:** ningun commit pusheado a master debe romper el deployment. Si el codigo no esta terminado pero compila + pasa tests, viaja detras de un flag OFF. Si rompe, no sale.
### Por que el registry esta exento ### Por que el registry esta exento
+41
View File
@@ -0,0 +1,41 @@
## Artefactos: termino colectivo
**"Artefacto"** es el termino paraguas para todo lo que vive en el registry pero NO es codigo reutilizable de `functions/` o `types/`. Sirve para no repetir "apps, analysis, vaults, projects, playgrounds" cada vez.
Tipos de artefacto:
| Tipo | Donde vive | Indexado en registry.db | Repo Gitea propio |
|---|---|---|---|
| **app** | `apps/`, `cpp/apps/`, `projects/<p>/apps/<a>/` | tabla `apps` | si (`dataforge/<a>`) |
| **analysis** | `analysis/<t>/`, `projects/<p>/analysis/<t>/` | tabla `analysis` | si (`dataforge/<t>`) |
| **vault** | `projects/<p>/vaults/<v>` (symlink) | tabla `vaults` | no (datos fuera del repo) |
| **project** | `projects/<p>/` | tabla `projects` | no (vive dentro de fn_registry) |
| **playground** | `<artefacto_padre>/playground/` | NO se indexa | no (vive dentro del padre) |
Caracteristicas comunes de los artefactos:
- NO son codigo reutilizable. La reutilizacion vive en `functions/`.
- Tienen ciclo de vida propio (crear, modificar, archivar, borrar).
- `pc_locations` los unifica via `entity_type` (app, analysis, project, vault).
- Pueden importar funciones del registry; el registry NUNCA importa de un artefacto.
### Cuando usar el termino
Usa "artefacto" cuando hablas de varios tipos a la vez o cuando la afirmacion aplica a todos:
- "Cada artefacto declara sus funciones del registry en su `.md`" (vale para apps y analyses).
- "Los artefactos no se importan desde `functions/`."
- "Esta regla aplica a cualquier artefacto desplegable" (apps + services).
Cuando hables de UN tipo concreto, usa el nombre concreto: "esta app...", "este analysis...". No abuses del termino paraguas — es para evitar listas, no para difuminar.
### Que NO es un artefacto
- `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/` — codigo reutilizable.
- `types/`, `python/types/`, `frontend/types/` — tipos del registry.
- `sources/` — repos externos clonados para extraer funciones (gitignored).
- `temp/` — workspace efimero, ni siquiera versionado.
- `subrepos/` — espejos de repos externos para referencia.
### Relacion con `pc_locations`
Los artefactos con presencia en disco (app, analysis, project, vault) ya estan unificados en `pc_locations` via la columna `entity_type`. Los **playgrounds** NO entran en `pc_locations` porque son hijos de otro artefacto y se mueven con el (no tienen identidad propia entre PCs).
+60
View File
@@ -0,0 +1,60 @@
## Capability groups: tags + paginas madre en docs/capabilities/
Un **capability group** es un cluster de >=3 funciones del registry que comparten un dominio operativo (ej. `notebook`, `metabase`, `deploy`). Cada grupo tiene un **tag plano** (sin prefijo) y una **pagina madre** en `docs/capabilities/<grupo>.md`. La pagina madre desbloquea el conjunto entero en un solo read.
### Para que existen
Sin grupos, Claude redescubre funciones via FTS5 una a una cada sesion ("¿como interactuo con Jupyter? ¿como subo deploy?"). Con grupos, Claude lee `docs/capabilities/<grupo>.md` y carga las 5-10 funciones del cluster con su ejemplo canonico — menos turnos perdidos en discovery.
### Convencion de tag
- **Slug del grupo** = tag plano. Ej: `notebook`, `metabase`, `android-emu`.
- **No prefijos** (`cap:`, `group:`). Ya hay namespacing implicito porque convivirian con tags semanticos sueltos.
- **Una funcion puede llevar varios tags de grupo** si pertenece a dos clusters (raro pero valido).
- Filtro MCP: `mcp__registry__fn_search query="" tag="notebook"` lista el grupo.
### Cuando crear grupo nuevo
- **Minimo 3 funciones** afines. Con 2 no compensa pagina madre — quedan tags sueltos.
- **Dominio operativo claro**: el grupo debe ser describible en 1 frase ("operar Jupyter colaborativo", "deploy via SSH+systemd").
- **Frontera neta** con grupos existentes. Si solapa con otro -> reorganizar, no duplicar.
### Como crear grupo
1. Anadir el tag al frontmatter `.md` de >=3 funciones afines. `fn index` lo registra.
2. Crear `docs/capabilities/<grupo>.md` con plantilla:
- **Lista de funciones**: tabla `ID | firma corta | que hace`.
- **Ejemplo canonico**: 1-2 bloques de codigo end-to-end con los IDs reales.
- **Fronteras**: que NO cubre el grupo.
- **Prerequisitos** y **notas** si aplica.
3. Anadir fila al `docs/capabilities/INDEX.md`.
4. Correr `fn doctor capabilities` para auditar drift.
### Auto-generacion
`fn doctor capabilities --update` (TBD) reescribe la tabla de funciones de cada pagina madre preservando bloques curated (`Ejemplo canonico`, `Fronteras`, `Notas`). Las secciones curated nunca se sobrescriben.
### Como Claude usa los grupos
Cuando una tarea cae en un dominio conocido:
1. `Read docs/capabilities/INDEX.md` para localizar grupo.
2. `Read docs/capabilities/<grupo>.md` para cargar funciones + ejemplo.
3. Solo si el grupo no cubre lo necesario, `mcp__registry__fn_search` para funciones sueltas.
4. Si el grupo deberia cubrir pero falta funcion -> `fn-constructor` + tagear con el grupo en el frontmatter.
### Auditoria
```bash
fn doctor capabilities # lista grupos + drift
fn doctor capabilities --json # para agentes
```
Comprueba:
- Tag con N >=3 funciones pero sin pagina madre -> "tag huerfano".
- Pagina madre sin tag respaldo -> "grupo fantasma".
- Funcion con tag de grupo pero la pagina madre no la lista (autogen desfasada) -> "drift".
### Relacion con dominios
Los **dominios** del registry (`core`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `tui`, `pipelines`, `browser`) son taxonomia ortogonal — un grupo puede atravesar varios dominios (ej. `deploy` toca `infra` y `shell`). NO renombrar dominio a grupo ni viceversa.
+263
View File
@@ -0,0 +1,263 @@
## Estandarizacion de apps C++ del registry
**Fuentes autoritativas:**
- `cpp/PATTERNS.md` — checklist y esqueleto del app shell (`fn::run_app`, AppConfig, panels, layouts, Settings, About).
- `cpp/DESIGN_SYSTEM.md` — identidad visual (`fn_tokens`, ThemeMode, equivalencias `@fn_library` ↔ C++).
Esta regla NO duplica esos documentos — los señala como obligatorios y añade convenciones estructurales que no aparecen alli.
### Scaffolder canonico — OBLIGATORIO
**REGLA DURA:** crear apps C++ nuevas SIEMPRE con `fn run init_cpp_app <name> [--project <p>] [--desc "..."]`. NUNCA escribir `main.cpp` + `CMakeLists.txt` + `app.md` desde cero a mano en `cpp/apps/` ni `projects/*/apps/`. Tampoco copiar otra app y renombrar — la deriva entre patrones es lo que estamos eliminando.
Si el scaffolder no cubre un caso (ej. necesitas plantilla diferente, layout custom desde el primer dia), **modificas el scaffolder**, no escribes la app a mano. La plantilla canonica es codigo, no decoracion.
Razones:
- Garantiza `cfg.about` + `cfg.log` + `cfg.panels` + framework defaults aplicados.
- Genera frontmatter `app.md` valido (framework, dir_path, repo_url) para `fn index`.
- Registra `add_subdirectory` en `cpp/CMakeLists.txt` (raiz o bloque `_DIR` para projects).
- Crea repo Gitea `dataforge/<name>` con master + commit inicial.
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
### 1. Ubicacion
| Caso | Donde vive |
|---|---|
| App independiente | `cpp/apps/<nombre>/` |
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
### 2. Estructura minima
```
<app_dir>/
CMakeLists.txt # usa add_imgui_app(target ...)
app.md # frontmatter de registro (ver §4)
main.cpp # entry: parseo de args + fn::run_app + render()
[data.{h,cpp}] # opcional: capa de datos (DB / HTTP / archivos)
[views.{h,cpp}] # opcional: composicion de paneles
[<modulo>.{h,cpp}] # opcional: dominio especifico
[vendor/] # opcional: deps no comunes (se prefieren las globales en cpp/vendor/)
[.git/] # cada app es su propio repo Gitea (ver §6)
```
**Reglas de split:**
- `main.cpp` SIEMPRE — punto de entrada con `int main()` + `fn::run_app(...)` + funcion `render()`.
- Si la app supera ~400 lineas en `main.cpp`, partir en `data.{h,cpp}` (carga/persistencia) + `views.{h,cpp}` (UI por panel).
- Modulos especificos del dominio en archivos propios (`compiler.cpp` en `shaders_lab`, `data_http.cpp` en `registry_dashboard`).
- NO crear archivos de "utilidades genericas" dentro de la app — eso va al registry como funcion (`cpp/functions/...`).
### 3. CMakeLists.txt
Patron canonico:
```cmake
add_imgui_app(<target>
main.cpp
[extra_modules.cpp]
# Funciones del registry usadas (paths absolutos):
${CMAKE_SOURCE_DIR}/functions/<dominio>/<funcion>.cpp
...
)
target_include_directories(<target> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(<target> PRIVATE [SQLite::SQLite3] [imgui_node_editor] ...)
if(WIN32)
set_target_properties(<target> PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
```
Reglas:
- Usar SIEMPRE la macro `add_imgui_app(target ...)` — gestiona enlace con `fn_framework` y copia de TTFs.
- Listar explicitamente cada `.cpp` del registry usado (no glob). Hace visible el grafo de dependencias.
- NO listar `tokens.cpp`, `icon_font.cpp`, `app_settings.cpp`, `app_about.cpp`, `fps_overlay.cpp`, `panel_menu.cpp`, `app_menubar.cpp`, `layouts_menu.cpp`, `gl_loader.cpp`, `layout_storage.cpp` — viven en `fn_framework` y dan multiple-definition si se duplican.
- En `WIN32`, marcar `WIN32_EXECUTABLE TRUE` para apps GUI (sin consola).
### 4. app.md (frontmatter)
Plantilla minima para apps C++:
```yaml
---
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
description: "Frase corta — lo que hace y por que existe."
tags: [imgui, ...] # si es service, anadir 'service'
uses_functions: # IDs del registry — el indexer NO deduce C++
- <nombre>_cpp_<dominio>
- ...
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/<name>" o "projects/<proyecto>/apps/<name>"
repo_url: "https://gitea-.../dataforge/<name>"
---
```
Reglas:
- `uses_functions` se rellena a mano con los IDs de las funciones del registry usadas en `CMakeLists.txt`. Auditar con: `sqlite3 registry.db "SELECT id FROM apps WHERE id='<id>';"` + revisar diffs.
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
- `repo_url` apunta al sub-repo en Gitea (ver §6).
### 5. Registro en `cpp/CMakeLists.txt`
Cada app nueva se registra al final de `cpp/CMakeLists.txt`:
```cmake
# --- <app_name> ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/<name>/CMakeLists.txt)
add_subdirectory(apps/<name>)
endif()
```
Para apps en proyectos (fuera del arbol `cpp/`):
```cmake
# --- <app_name> (lives in projects/<proj>/apps/) ---
set(_<NAME>_DIR ${CMAKE_SOURCE_DIR}/../projects/<proj>/apps/<name>)
if(EXISTS ${_<NAME>_DIR}/CMakeLists.txt)
add_subdirectory(${_<NAME>_DIR} ${CMAKE_BINARY_DIR}/apps/<name>)
endif()
```
El `if(EXISTS ...)` hace el registro tolerante a apps no clonadas (cada app es sub-repo separado).
### 6. Sub-repo Gitea (TBD obligatorio)
Cada app C++ es su propio repo en `dataforge/<name>` con branch `master`. Esto significa:
- El directorio `<app_dir>/` esta en el `.gitignore` de `fn_registry` (excepto `app.md`).
- El propio directorio tiene `.git/` apuntando al sub-repo.
- TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/<NNNN>-<slug>` o `quick/<slug>`, mergear a `master` con `--no-ff`.
- Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`.
### 7. Convencion `local_files/` — separacion de distribuible vs estado local
**OBLIGATORIO**: TODA app coloca sus archivos escribibles bajo
`<exe_dir>/local_files/`. Los archivos distribuibles (`.exe`, `.dll`,
`.ttf`, `enrichers/`, `runtime/`) viven directos en `<exe_dir>/`.
```
<exe_dir>/
├── <app>.exe
├── duckdb.dll, *.ttf, runtime/, enrichers/ ← read-only, ships con el zip
└── local_files/ ← writable, per-PC
├── imgui.ini ← gestionado por fn::run_app
├── app_settings.ini ← gestionado por fn_ui::settings_*
└── <lo que la app escriba> ← usar fn::local_path("nombre")
```
`fn::run_app` lo gestiona automaticamente para `imgui.ini` y
`app_settings.ini` y migra desde `<exe_dir>/` o `cwd` si vienen de
una version previa.
Apps que escriban archivos extra (DBs, caches, proyectos del
usuario) **DEBEN** usar `fn::local_path("nombre")` al construir
sus paths. Ejemplo:
```cpp
// MAL
sqlite3_open("graph_explorer.db", &db);
fopen("graph_explorer.ini", "r");
// BIEN
sqlite3_open(fn::local_path("graph_explorer.db"), &db);
fopen(fn::local_path("graph_explorer.ini"), "r");
```
API en `cpp/framework/app_base.h`:
- `fn::exe_dir()` — directorio del ejecutable.
- `fn::local_dir()``<exe_dir>/local_files/`, creado on-demand.
- `fn::local_path(name)``<local_dir>/<name>`.
- `fn::migrate_to_local_files(names, n)` — mueve archivos viejos.
Beneficios:
- Carpeta del .exe limpia para distribuir (zip portable).
- Reset trivial (basta borrar `local_files/`).
- Separacion clara para backup/sync (solo `local_files/` es propio del PC).
### 7.1 Anti-jitter automatico (AltSnap, tiling WMs)
`fn::run_app` aplica tres capas de proteccion contra jitter al mover la
ventana con herramientas externas (AltSnap en Windows, snap-assist, tiling
WMs). Activado por defecto, sin opt-in:
1. **GLFW pos/size callbacks**`vp->Pos/Size` se sincronizan al instante
con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame).
2. **Per-frame viewport sync** al inicio del main loop — cubre viewports
secundarios (paneles drag-out) que la backend crea dinamicamente.
3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE`
/ `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras
el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`,
replicando el contrato del title-bar drag native (DefWindowProc bloquea
el hilo, DWM compositor mueve el framebuffer existente).
Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases:
- `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta
`vp->Pos` sigue OS dentro de 1px.
- `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` +
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta
que `render()` no se llama durante el bracket.
Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`.
NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app
necesita renderizar incluso durante external move (caso raro: telemetria
en vivo, video stream), tendria que evitar el bypass — actualmente no hay
flag para desactivarlo (anadir `cfg.pause_on_external_sizemove = true` por
default si surge necesidad).
### 8. Convenciones de runtime
Cumplir el checklist completo de `cpp/PATTERNS.md`. Resumen de lo que NUNCA debe aparecer en una app:
| Anti-patron | Sustituir por |
|---|---|
| `glfwInit()` en `main` | `fn::run_app(cfg, render)` |
| `ImGui::StyleColorsDark()` | `cfg.theme = ThemeMode::FnDark` (default) |
| `ImVec4(0.5,0.5,0.5,1)` | `fn_tokens::colors::*` |
| `ImGui::Begin(u8"\xEF...")` | `ImGui::Begin(TI_HOME " ...")` |
| Menubar inline cada frame | `cfg.panels` + `cfg.layouts_cb` |
| About hardcoded en un panel | `cfg.about = {...}` |
| `gl*` directo sin loader | `cfg.init_gl_loader = true` |
| Tabla SQLite en la raiz del repo | `<app_dir>/<app>.db` (operations.db es solo para entities/relations/executions) |
| `fopen("foo.ini", ...)` con path relativo | `fopen(fn::local_path("foo.ini"), ...)` (ver §7) |
### 8. Tests visuales (recomendado, no obligatorio)
Si la app tiene componentes que se quieren proteger contra regresiones visuales, anadir un demo en `cpp/apps/primitives_gallery/demos_<dominio>.cpp` que use los mismos componentes/funciones del registry. El sistema de capture-and-compare de `primitives_gallery --capture` funciona como golden-image gate (ver final de `cpp/PATTERNS.md`).
### 9. Decisiones que cada app debe tomar y documentar en su `app.md`
- `viewports`: `true` (default) si las ventanas pueden arrastrarse fuera del main; `false` si la app necesita estar siempre embebida.
- `init_gl_loader`: `true` si llama `gl*` directo (renderers GPU custom como `graph_renderer`); `false` si solo usa ImGui/ImPlot.
- `about` info: nombre, version (semver), descripcion 1 frase.
- Persistencia: `<app>.db` SQLite junto al exe; nunca tocar `registry.db` ni `operations.db` salvo lectura.
- Modo CLI: si la app acepta args, documentarlos en el `app.md` con ejemplos.
### 10. Layouts persistentes (default)
`fn::run_app` provee menu Layouts (Save current as.../Apply/Delete/Reset) sin
codigo. Crea `<exe_dir>/local_files/layouts.db` (tabla `imgui_layouts` +
`layout_meta`) y persiste el `imgui.ini` serializado por nombre.
**Restore-on-open / save-on-close (1.1.0+):** al cerrar la app, el slot del
layout activo se reescribe con el `imgui.ini` actual (los retoques de
docking sobreviven). Al abrir, si habia un layout activo persistido en
`layout_meta.last_active`, se carga en el primer frame. Si la app no usa
named layouts (nunca clico Save/Apply), el comportamiento sigue siendo el
de antes: `imgui.ini` es la unica fuente.
- App nueva: nada que tocar — Layouts viene activo.
- App quiere personalizar `on_reset` (ej. re-mostrar paneles especificos como
`shaders_lab`): abre su propio `LayoutStorage`, llama
`layout_storage_make_callbacks`, override `on_reset`, y pasa
`cfg.layouts_cb = &cb`. Cuando se pasa `layouts_cb`, el auto-storage se
desactiva y la app es responsable de `layout_storage_apply_pending` al
inicio de su `render`.
- App headless / capture mode: `cfg.auto_layouts = false`.
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
`local_files/`).
+165
View File
@@ -0,0 +1,165 @@
## Migraciones de BBDD: nunca perder datos
**Regla absoluta:** todo cambio de schema en SQLite (apps con `kanban.db`, `operations.db` propia, registry.db, etc.) DEBE ir en un archivo de migración versionado. Nunca borrar/recrear tablas, nunca cambiar tipos sin proceso seguro, nunca confiar en "borra el .db y vuelve a empezar".
### Por que
- Las apps almacenan **datos vivos** (cards, entities, executions, assertions, columns, sessions).
- Borrar = perder horas/dias/semanas de trabajo del usuario.
- Lo que es trivial en dev (`rm operations.db`) es destructivo en produccion (deploys + sync entre PCs).
- Sync entre PCs (`fn sync`, `/full-git-pull`) trae bases de datos de otros equipos: si tu schema asume tabla recreada, los datos del otro PC desaparecen.
### Patrones obligatorios
#### 1. Archivos numerados en `migrations/`
Cada cambio de schema = un archivo nuevo `migrations/NNN_<accion>.sql`. Numeracion zero-padded de 3 digitos. Nombre descriptivo.
```
apps/<app>/migrations/
001_init.sql # CREATE TABLE inicial (no se modifica nunca)
002_add_stickers.sql # ALTER TABLE cards ADD COLUMN stickers
003_add_assignees.sql # ALTER TABLE cards ADD COLUMN assignee_id
004_create_lock_history.sql # CREATE TABLE card_lock_history
...
```
#### 2. Solo operaciones aditivas seguras
| Operacion | Seguro | Notas |
|---|---|---|
| `CREATE TABLE IF NOT EXISTS` | si | idempotente |
| `CREATE INDEX IF NOT EXISTS` | si | idempotente |
| `ALTER TABLE ... ADD COLUMN` | si | aditivo, default obligatorio |
| `INSERT INTO ... ON CONFLICT IGNORE` | si | seed data idempotente |
| `DROP TABLE` | NO | destructivo |
| `DROP COLUMN` | NO | destructivo (SQLite < 3.35 ni siquiera lo soporta) |
| `ALTER TABLE ... RENAME COLUMN` | precaucion | rompe codigo viejo si rollback |
| `ALTER TABLE ... DROP/ALTER constraint` | NO sin backup | requiere recreate-and-copy |
Si necesitas cambiar tipo, eliminar columna, o cambiar PK: hacer **migracion en pasos** (Branch by Abstraction):
1. Crear nueva columna/tabla con la forma deseada (migration N).
2. App escribe en ambas (migration N+1, codigo).
3. Backfill de datos viejos (migration N+2, script).
4. App lee solo de la nueva (migration N+3, codigo).
5. Eliminar la vieja (migration N+4, despues de tener backups verificados).
Cada paso = una rama TBD corta + commit + verificacion. Nunca un solo PR que rompa lectores.
#### 3. Aplicacion idempotente al arrancar
La app aplica todas las migraciones en orden al iniciar. Patron canonico (Go):
```go
//go:embed migrations/*.sql
var migrationsFS embed.FS
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 err }
if _, err := conn.Exec(string(b)); err != nil {
// SQLite ALTER TABLE ADD COLUMN no es idempotente nativamente.
// Si ya existe, ignorar el error de "duplicate column".
if !strings.Contains(err.Error(), "duplicate column") &&
!strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("%s: %w", f, err)
}
}
}
return nil
}
```
Alternativa: tabla `_migrations` con las versiones aplicadas (mas robusta para schemas grandes). Para apps pequeñas (kanban, operations.db), bastan los archivos numerados + `IF NOT EXISTS` / catch de "duplicate column".
#### 4. Migracion + cambios en codigo en el mismo commit
Cuando añades una columna:
- `migrations/NNN_<accion>.sql` (nueva)
- `db.go` (lee/escribe la columna)
- `types.ts` (frontend type)
- Tests
Todo en el mismo commit/rama. Si solo mergeas la migracion pero no el codigo, otros PCs aplican la migracion al sync y luego el codigo viejo no la usa. OK. Si mergeas el codigo sin la migracion, la app peta al arrancar en otros PCs. Mal. **Migracion antes que codigo en el orden de archivos** (no de tiempo).
#### 5. Tests sobre la migracion
Cada migracion debe tener test que:
- Arranca con DB vacia → aplica todas → verifica schema.
- Arranca con DB en estado N-1 (datos previos) → aplica migracion N → verifica que los datos se conservan.
Esto detecta migraciones destructivas antes de mergear.
### Que NO hacer
| Anti-patron | Consecuencia |
|---|---|
| Borrar `*.db` durante dev y commitear "schema actualizado" | Otros PCs pierden datos al sync. |
| Modificar `001_init.sql` para añadir columnas | Las DBs ya creadas no se actualizan. Datos divergentes. |
| `DROP TABLE x; CREATE TABLE x ...` | Borra todo lo que el usuario tenga. |
| Usar `ensureColumns` sin archivo SQL paralelo | El cambio de schema vive solo en codigo Go, no auditable, no migrable manualmente. |
| Cambiar tipo de columna in-place | SQLite necesita recreate-and-copy. Asume que pierde datos si no se hace bien. |
| "fn index" como solucion para regenerar registry.db | OK para `registry.db` (regenerable). NUNCA para `operations.db`, `kanban.db`, etc. |
### Casos especiales
#### registry.db (raiz del fn_registry)
`registry.db` SE PUEDE regenerar con `fn index` desde los `.go` y `.md`. Para cambios de schema del registry: actualizar `registry/migrations.go` o el codigo de creacion + `fn index`. NO hace falta archivo de migracion porque la fuente de verdad son los `.md`/`.go`. Excepcion: tablas con datos vivos (`proposals`, `pc_locations`) — esas SI requieren migracion preservando datos.
#### operations.db (por app)
Cada app tiene su `operations.db` con entities/relations/executions. Schema definido en `fn_operations/`. Cambios al schema → archivo de migracion en `fn_operations/migrations/` aplicado al abrir la BD. Idempotente.
#### apps con BD propia (kanban, etc.)
Mismo patron: `apps/<app>/migrations/NNN_*.sql`, embebido y aplicado al arrancar.
### Comandos utiles
```bash
# Ver schema actual
sqlite3 apps/kanban/operations.db ".schema"
# Ver columnas de una tabla
sqlite3 apps/kanban/operations.db "PRAGMA table_info(cards);"
# Backup antes de migracion arriesgada
sqlite3 apps/kanban/operations.db ".backup apps/kanban/operations.db.bak.$(date +%Y%m%d)"
# Aplicar una migracion manual (si la app no esta corriendo)
sqlite3 apps/kanban/operations.db < apps/kanban/migrations/00X_<accion>.sql
# Listar archivos de migracion en orden
ls apps/kanban/migrations/*.sql | sort
```
### Resumen
- Cada cambio de schema = archivo numerado nuevo en `migrations/`.
- Aditivo siempre que se pueda. Destructivo solo en pasos verificados con backup.
- App aplica migraciones al arrancar, idempotente.
- Migracion + codigo + tests en el mismo commit.
- Nunca borrar `.db` para "arreglar" schema. Nunca modificar migraciones existentes.
### Estado retroactivo (2026-05-09)
Inventario de BDs del ecosistema y conformidad con la regla:
| Repo / App | BD | `migrations/` | Estado |
|---|---|---|---|
| `registry/` | `registry.db` | si (11 archivos) | ✓ |
| `fn_operations/` | `operations.db` por app | si (4 archivos) | ✓ |
| `apps/kanban/` | `operations.db` (kanban) | si (5 archivos: 001 init, 002 stickers, 003 columns_extras, 004 cards_extras, 005 history_actor) | ✓ |
| `apps/deploy_server/` | `operations.db` (deploys) | si (2 archivos: 001 init, 002 target_extras) | ✓ |
| `apps/dag_engine/store/` | DB del dag_engine | si (001_init) | ✓ |
| `projects/element_agents/.../shell/memory/` | memoria del agente | si (001_init) | ✓ |
| `projects/osint_graph/apps/graph_explorer/` | DBs C++ inline (project_manager, layout_store, jobs, node_groups) | NO | **pendiente** — refactor C++ multi-archivo, mover schema inline a `migrations/*.sql` aplicado al abrir cada DB. |
Las apps marcadas ✓ usan el patron canonico `embed.FS + applyMigrations()` (Go) o equivalente. La C++ pendiente requiere ronda dedicada — tracker via issue cuando se aborde.
`apps/kanban/db.go::ensureColumns` se mantiene como **backstop idempotente** para DBs muy antiguas creadas antes del refactor de migraciones. NO añadir columnas nuevas alli — siempre via archivo SQL.
+42
View File
@@ -0,0 +1,42 @@
## Delegacion: spawn fn-constructor en vez de escribir inline
**REGLA DURA.** Si vas a escribir logica reutilizable inline en un artefacto (app, analysis, playground) o heredoc, STOP y delega a `fn-constructor`. La misma sesion debe crear + usar la funcion. No acumular huerfanas.
### Cuando un patron es candidato a funcion
- Aparece >=2 veces en esta sesion o en heredocs recientes.
- Firma generica (no depende de tipos internos del artefacto).
- 1 responsabilidad clara (CRUD, parse, transform, http call, formato fijo, etc.).
- No es one-liner idiomatico de stdlib (`time.Now().UTC().Format(...)` queda fuera).
### Flujo obligatorio (mismo turno)
1. **Detectar**. Si vas a escribir >=5 lineas de logica reutilizable inline -> STOP.
2. **Spawn `fn-constructor` inmediato** via `Agent(subagent_type="fn-constructor", ...)`:
- **Sin preguntar al usuario** (autorizado por defecto).
- Si hay >1 funcion independiente -> una sola llamada al Agent tool con **N tool_use blocks paralelos** en el mismo mensaje. NO serializar.
3. **Tagear con grupo de capacidad** al menos UN tag de grupo (`notebook`, `metabase`, `deploy`, etc.). Ver `capability_groups.md`.
4. **`fn index`** para registrar.
5. **Importar + invocar en el mismo turno** — no dejar funcion huerfana recien creada.
6. **Auto-verificar** con `fn doctor uses-functions` y `fn doctor unused` si tocas >=3 funciones nuevas.
### Anti-patrones auditables
| Anti-patron | Consecuencia | Sustituir por |
|---|---|---|
| Escribir helper inline en artefacto en vez de delegar | Reinvento por sesion | Spawn fn-constructor |
| Crear N funciones serialmente | Latencia x N | Multiples `Agent()` en mismo mensaje |
| Crear funcion y no usarla en el turno | Huerfana desde dia 1 (`calls_90d=0`) | Importar + invocar antes de cerrar turno |
| Crear funcion sin tag de grupo | Imposible descubrir en bloque proxima sesion | Anadir tag de grupo (capability group) |
| Reescribir en heredoc logica que ya existe | Capitalizacion perdida | `mcp__registry__fn_search` antes de escribir |
### Excepciones
- **Logica de dominio especifica del artefacto** (CRUD de tabla concreta, layout de UI, flujo unico de la app) -> queda en el artefacto. Solo lo reutilizable se delega.
- **Stub temporal con `not implemented`**: aceptable si la dependencia externa no esta disponible. Documentar en `.md` (ver `stubs.md`).
### Telemetria
Cada `code_writes` + `calls` se registra en `call_monitor/operations.db` (issue 0085). Vista `session_capability_growth` mide ratio creadas vs usadas por sesion. Hook `UserPromptSubmit` inyecta `CAPABILITY-GROWTH: created_this_session=X used=Y orphan=Z` en cada turno.
Si `orphan>0` al cerrar la sesion -> revisar: o la funcion era especulativa (no debio crearse) o falta integrarla en el codigo del artefacto.
+162
View File
@@ -0,0 +1,162 @@
## Validacion end-to-end de apps (bucle reactivo, fase 4)
**Contrato obligatorio para apps que vayan a master con gate automatico**: declarar `e2e_checks` en su `app.md`. Sin contrato, `fn-analizador` no puede validar y la app cae al modo "manual": el humano sigue iterando.
Ver tambien: `apps_tbd.md`, `feature_flags.md`, issue 0068.
### Por que
El bucle reactivo del registry tiene 5 fases. Las 3 primeras (`fn-constructor`, `fn-executor`, `fn-recopilador`) cubren CONSTRUIR/EJECUTAR/RECOPILAR. La fase 4 (ANALIZAR) y la 5 (MEJORAR) no funcionan sin un contrato explicito de "como sabe el agente que esta app esta sana". Ese contrato es `e2e_checks`.
### Donde vive
En el frontmatter de cada `app.md`, lista `e2e_checks`. Convencion: `id` unico por check, ejecucion en orden declarado, falla = stop o continue segun severidad (TBD por implementar).
### Tipos de check
| Campo | Que hace |
|---|---|
| `id` | Identificador unico del check dentro de la app (`build`, `smoke`, `tests_unit`, ...) |
| `cmd` | Comando shell. Exit 0 = pass salvo override de `expect_exit`. |
| `health` | URL HTTP. Hace GET, espera 200, util tras un `cmd` que arranca un servicio en background (con `&`). |
| `ref` | Referencia a otro agente / funcion del registry (ej. `fn-recopilador:apps/X`, `fn-doctor:artefacts`). |
| `timeout_s` | Timeout en segundos. Default 60. |
| `expect_exit` | Codigo de salida esperado (default 0). |
| `expect_stdout_contains` | Substring que debe aparecer en stdout. |
| `expect_stdout_json` | JSONPath o key=value que debe satisfacer la salida. |
| `severity` | `critical` (default) o `warning`. Critical = bloquea merge; warning = registra y sigue. |
### Patrones por stack
#### Go service con frontend embebido
```yaml
e2e_checks:
- id: build_frontend
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
timeout_s: 180
- id: build_backend
cmd: "CGO_ENABLED=1 go build -tags fts5 -o myapp ."
- id: smoke
cmd: "./myapp --port 8200 --db /tmp/myapp_e2e.db &"
health: "http://127.0.0.1:8200/api/health"
- id: tests
cmd: "go test -tags fts5 -count=1 ./..."
```
#### C++ ImGui app
```yaml
e2e_checks:
- id: build
cmd: "cmake --build build --target myapp -j"
timeout_s: 300
- id: self_test
cmd: "./build/myapp --self-test"
timeout_s: 30
- id: pytest
cmd: "cd tests && python3 -m pytest -x -q"
```
Apps C++ deben implementar `--self-test` que arranca, verifica subsistemas (GL loader, fonts, DBs locales), y sale con codigo 0/1.
#### Python pipeline / CLI
```yaml
e2e_checks:
- id: import
cmd: "python3 -c 'import myapp'"
- id: cli_help
cmd: "python3 -m myapp --help"
expect_stdout_contains: "usage:"
- id: dry_run
cmd: "python3 -m myapp --dry-run --input examples/sample.json"
```
#### App con operations.db
Anadir siempre:
```yaml
- id: ops_audit
ref: "fn-recopilador:apps/myapp"
```
Esto invoca al recopilador en modo audit sobre `apps/myapp/operations.db`.
### Reglas
1. **Idempotente**: cada check debe poderse correr N veces sin efectos secundarios. Usar BDs en `/tmp/`, puertos altos, `--port 0` cuando se pueda.
2. **Sin credenciales reales**: ningun check toca produccion ni servicios externos sensibles. Si necesita HTTP de prueba, usar `httpbin.org` o un mock local.
3. **Tiempo acotado**: cada check declara `timeout_s`. Suma total de la app < 10 min como objetivo razonable.
4. **Determinista**: si el check depende de red flaky, marcalo `severity: warning` o usalo solo como diagnostico, no como gate.
5. **Cleanup implicito**: si el check arranca un proceso en background (`&`), debe morir al final. `fn-analizador` mata el grupo de procesos al terminar la suite.
### Como diseñar `e2e_checks` para una app existente
`fn-recopilador` tiene un modo `design-e2e <app_id>` que:
1. Inspecciona `app.md` (lang, framework, entry_point, uses_functions).
2. Revisa estructura del directorio (presencia de `tests/`, `frontend/`, `Makefile`, `CMakeLists.txt`, etc.).
3. Audita `operations.db` (si existe) para sugerir `ops_audit`.
4. Devuelve bloque `e2e_checks_suggested:` listo para copiar al `app.md` tras revision humana.
Comando indicativo:
```
Agent(subagent_type="fn-recopilador",
prompt="design-e2e apps/<app>")
```
El recopilador NO escribe directo al `app.md`; deja la propuesta para que el humano apruebe (similar a `proposals`).
### Adopcion gradual
- Apps SIN `e2e_checks` declarado: `fn doctor` muestra warning, no bloquea nada.
- Apps CON `e2e_checks`: `fn-analizador` corre la suite. Si critical falla → `fn-mejorador` crea proposal. Gate opcional en `/git-push`.
- Pilotos iniciales: `apps/kanban`, `projects/osint_graph/apps/graph_explorer`. Resto de apps van migrando segun necesidad.
### Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| `cmd: "make test"` con make-target opaco | Ilegible. El check debe ser ejecutable directo y auditable. |
| Check que tarda > 5 min sin razon (smoke pesado) | Bloquea iteracion. Mover a CI nocturno con tag `slow`. |
| Smoke que toca produccion | Riesgo. Smoke usa BD efimera, puertos altos, mocks. |
| `expect_stdout_contains: ""` | Vacio = siempre pass. No es un check. |
| Anidar checks (uno depende de side-effects de otro sin declararlo) | Frigil. Cada check arranca lo que necesita. |
| Usar `e2e_checks` como sustituto de tests unitarios | Son cosas distintas. Unit tests viven en `*_test.go`/`pytest`. e2e valida que el sistema arranque y haga su trabajo. |
### Tabla `e2e_runs` en operations.db
Cada corrida de `fn-analizador` se persiste:
```sql
CREATE TABLE IF NOT EXISTS e2e_runs (
id TEXT PRIMARY KEY,
app_id TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER,
status TEXT NOT NULL, -- pass|fail|partial
checks_total INTEGER NOT NULL,
checks_pass INTEGER NOT NULL,
checks_fail INTEGER NOT NULL,
summary_json TEXT NOT NULL
);
```
Migracion: `fn_operations/migrations/006_e2e_runs.sql` (issue 0068, paso 3).
### Output canonico de fn-analizador
Tabla caveman, una linea por check:
```
build ✓ 42s
smoke ✓ 0.8s
ops_audit ✓
tests ✗ 12s exit 1, 3/45 failures
assertion:R1 ✗ warning duration drift +47% vs p50
golden:home ✓
```
Rojo cuando `severity: critical` y status fail. Esto es lo que el agente principal lee y reenvia al humano.
+191
View File
@@ -0,0 +1,191 @@
## Feature flags: enviar codigo incompleto a master sin romperlo
Doctrina oficial de **trunk-based development**: master siempre desplegable. Cuando una feature no cabe en una sola rama corta, o cuando hay WIP que no esta terminado pero el resto si, **el codigo viaja detras de un flag OFF**. Asi master sigue verde y el codigo a medio terminar no llega a usuarios reales.
Refs: [trunkbaseddevelopment.com/feature-flags/](https://trunkbaseddevelopment.com/feature-flags/), [trunkbaseddevelopment.com/branch-by-abstraction/](https://trunkbaseddevelopment.com/branch-by-abstraction/).
### Cuando usar feature flag
| Situacion | Accion |
|---|---|
| Feature multi-issue (`0015a`, `0015b`, `0015c`) que llevan dias | Cada sub-issue mergea con flag OFF. Ultimo sub-issue activa flag. |
| Refactor grande tipo "Branch by Abstraction" (ej. cambiar driver DB) | Crear abstraccion + impl nueva con flag. Eliminar antigua + flag al final. |
| Cambio con riesgo en produccion que necesita rollback rapido | Flag para apagar sin redeploy. |
| Despliegue gradual (un PC primero, luego todos) | Flag por PC/usuario/grupo. |
| WIP detectado al cerrar otra rama | Envolver el codigo a medias en flag OFF, mergear, terminar despues. |
### Cuando NO usar feature flag
- Bug fix autocontenido → mergear directo, sin flag.
- Refactor que cabe en una rama corta → directo.
- Docs, comments, type signatures → directo.
- Codigo que no compila o no pasa tests → **NO viaja a master, ni con flag**. Flag protege codigo terminado, no roto.
### Flag != WIP
- **WIP**: codigo a medias, no compila o no testea. NO va a master.
- **Flag**: codigo terminado y testeado, pero no expuesto al usuario. SI va a master.
Si hay 80% terminado y 20% pendiente: completar al menos un slice vertical funcional (compila, pasa tests, se puede activar end-to-end), mergear con flag OFF, dejar el 20% para otra rama. NO mergear el 20% sin proteger.
### Archivo de flags
`dev/feature_flags.json` en la raiz del repo (registry o app). Formato canonico:
```json
{
"flags": {
"<flag-name>": {
"enabled": false,
"issue": "0063",
"description": "Descripcion 1 linea de la feature",
"added": "2026-05-08",
"enabled_at": null
}
}
}
```
Cuando se activa: cambiar `enabled: true` y rellenar `enabled_at` con fecha. Cuando la feature ya es estable y no necesita rollback (semanas/meses despues): borrar el flag y todas sus ramas condicionales del codigo. **Los flags caducan**; documentar fecha de revision para evitar que se acumulen.
### Patron por stack
#### Go (apps/services)
Cargar flags al arrancar. Patron simple — hashmap en memoria + helper `Enabled(name)`:
```go
// pkg/flags/flags.go (puro hasta donde se pueda)
package flags
import (
_ "embed"
"encoding/json"
)
type Flag struct {
Enabled bool `json:"enabled"`
Issue string `json:"issue"`
Description string `json:"description"`
}
type Flags struct{ Flags map[string]Flag `json:"flags"` }
func Parse(b []byte) (Flags, error) {
var f Flags
err := json.Unmarshal(b, &f)
return f, err
}
func (f Flags) Enabled(name string) bool {
flag, ok := f.Flags[name]
return ok && flag.Enabled
}
```
Uso:
```go
if flags.Enabled("kanban-stickers") {
registerStickerRoutes(router)
}
```
Para flags en frontend embebido: serializar a `/api/flags` y leer desde el cliente (ver TS).
#### TypeScript / React
Inyectar en build (Vite) o exponer endpoint `/api/flags`:
```ts
// src/flags.ts
let cache: Record<string, boolean> | null = null;
export async function loadFlags(): Promise<Record<string, boolean>> {
if (cache) return cache;
const res = await fetch("/api/flags");
const data = await res.json();
cache = Object.fromEntries(Object.entries(data.flags).map(([k, v]: [string, any]) => [k, !!v.enabled]));
return cache;
}
export function isEnabled(name: string): boolean {
return !!(cache?.[name]);
}
```
Render condicional:
```tsx
{isEnabled("kanban-stickers") && <StickerToolbar ... />}
```
Para flags en build-time (constantes del bundle), usar `import.meta.env.VITE_FLAG_X` o un plugin Vite que reemplace simbolos.
#### Bash / pipelines
Lectura directa con `jq`:
```bash
ENABLED=$(jq -r '.flags["my-feature"].enabled' dev/feature_flags.json)
if [ "$ENABLED" = "true" ]; then
run_new_path
else
run_legacy_path
fi
```
#### Python
```python
import json
from pathlib import Path
def flags() -> dict:
return json.loads(Path("dev/feature_flags.json").read_text())["flags"]
def enabled(name: str) -> bool:
f = flags().get(name)
return bool(f and f.get("enabled"))
if enabled("nuevo-pipeline"):
run_new()
else:
run_legacy()
```
### Branch by Abstraction (caso especial)
Para cambios grandes (ej. swap iBatis → Hibernate, swap libreria, swap protocolo):
1. **Abstraer**: crear interfaz que envuelve la implementacion antigua. Master sigue verde con la antigua. Mergear.
2. **Implementar nueva**: bajo la misma interfaz, detras de flag OFF. Tests para ambas. Mergear.
3. **Activar**: flip flag a ON en commit pequeño. Si rompe, flip OFF de inmediato.
4. **Eliminar antigua**: borrar codigo legacy + flag + abstraccion. Mergear.
Cada paso es un merge corto, master nunca esta roto, hay rollback en cada punto.
### Reglas operativas
- **Un flag = un proposito**. Si necesitas dos toggles independientes, usa dos flags.
- **Flag bool por defecto**. Si necesitas A/B/C, sigue siendo bool por nombre (`my-feature-v2`, `my-feature-v3`).
- **Tests con flag ON y OFF**. CI corre ambos paths cuando el flag toca codigo critico.
- **Documenta en el issue**: que flag protege que codigo, cuando se va a activar, cuando se va a borrar.
- **No anidar flags**. Si una rama esta detras de dos flags, simplifica.
- **Borra el flag**. Cuando la feature lleva semanas activa sin rollback, eliminar el flag es trabajo real, no opcional.
### Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| `if (flag) { ... } else { ... }` esparcido por 30 archivos | Imposible de borrar. Usar inyeccion / strategy pattern. |
| Flag que lleva 6 meses ON sin borrar | Deuda tecnica. Borrar el flag y simplificar. |
| Flag para WIP que no compila | Master roto. Eso no es flag, es WIP — no debe estar en master. |
| Flag condicional sobre tipos / esquemas DB | Migrations son irreversibles. No se "apaga" una columna. Usar branch-by-abstraction sobre la lectura/escritura, no sobre el schema. |
| Flag con nombre del autor o del issue (`lucas-experiment`, `flag-0063`) | Sin contexto al releerlo. Nombrarlo por la feature: `kanban-stickers`. |
### Comandos relacionados
- `/git-branch` — crea rama desde master.
- `/git-push` — merge --no-ff + push.
- Para registrar / activar un flag: editar `dev/feature_flags.json` directamente y commitear con el codigo correspondiente. No hay CLI dedicada todavia.
+78
View File
@@ -0,0 +1,78 @@
## fn doctor: diagnostico del registry y artefactos
`fn doctor` es el entrypoint unico para auditar la salud del sistema de forma read-only. Compone funciones del registry (`functions/infra/`) y formatea su salida. No modifica nada.
### Cuando usar
- Despues de un deploy: confirmar que servicios siguen vivos y artefactos intactos.
- Despues de `git pull` o `fn sync`: detectar drift entre BD y disco.
- Antes de `fn index` masivo: confirmar que apps Go/Py siguen declarando bien sus deps.
- Periodicamente (cron): listar funciones del registry sin consumidores para limpiar.
- Como gate antes de crear proposals: si `fn doctor` esta verde, las metricas del bucle reactivo son fiables.
### Comandos
```bash
fn doctor # Corre TODOS los checks (artefacts + services + sync + uses-functions + unused + cpp-apps)
fn doctor artefacts # Solo artefactos: git/venv/app.md/upstream
fn doctor services # Solo apps con tag 'service' + systemctl + puerto
fn doctor sync # Solo drift pc_locations BD vs disco local
fn doctor uses-functions # Solo audit imports reales vs uses_functions
fn doctor unused # Solo funciones huerfanas del registry
fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado)
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
```
### Mapeo subcomando → funcion del registry
| Subcomando | Funcion |
|---|---|
| `artefacts` | `artefact_doctor_go_infra` |
| `services` | `services_status_go_infra` |
| `sync` | `pc_locations_drift_go_infra` |
| `uses-functions` | `audit_uses_functions_go_infra` |
| `unused` | `find_unused_functions_go_infra` |
| `cpp-apps` | `audit_cpp_apps_go_infra` |
Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente.
### Salida
Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable para `jq`, agentes o pipes.
### Idempotente y seguro
- Read-only: ningun subcomando escribe, mata procesos ni cambia estado.
- `services` abre conexiones TCP a `127.0.0.1:<port>` con timeout 500ms — no genera trafico saliente.
- `artefacts` ejecuta `git rev-parse @{u}` con timeout 3s por artefacto.
### Acciones complementarias (NO son `fn doctor`)
`fn doctor` solo diagnostica. Las acciones derivadas son verbos separados:
| Si `fn doctor` reporta... | Accion |
|---|---|
| `directory_missing` | Marcar `pc_locations.status='missing'` o re-clonar via `/full-git-pull` |
| `git_not_initialized` | `gitea_create_repo_bash_infra` + `ensure_repo_synced_bash_infra` |
| `venv_broken_path` | `cd <analysis_dir> && rm -rf .venv && uv sync` |
| `service active=inactive` | `systemctl --user start <unit>` o investigar logs |
| `port not listening` | `port_kill_bash_infra <port>` (si zombie) y relanzar |
| `missing_in_app_md` | Editar `app.md` y añadir el ID a `uses_functions` |
| `unused` (funcion huerfana) | Decidir: usar, deprecar (tag), o borrar |
| `manual_app_menubar_call` | Borrar `fn_ui::app_menubar(...)` del render — el framework ya lo dibuja |
| `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio |
| `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` |
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
### Para agentes
Patron recomendado tras una accion no trivial (deploy, sync, mass edit):
```bash
fn doctor --json > /tmp/doctor.json
# Agente parsea JSON, decide si crear proposals o avisar al humano
```
Si el agente quiere actuar sobre los hallazgos, abre proposals con `fn proposal add` referenciando los IDs afectados — NO toca artefactos directamente sin aprobacion humana.
+16
View File
@@ -9,3 +9,19 @@ El sistema de UI es Mantine v9. Todos los componentes de @fn_library wrappean co
**Iconos:** Se usa `@tabler/icons-react` (el set nativo de Mantine), no lucide-react. **Iconos:** Se usa `@tabler/icons-react` (el set nativo de Mantine), no lucide-react.
**Layout:** Se usan los componentes de layout de Mantine: `Group`, `Stack`, `Grid`, `Flex`, `SimpleGrid`, `AppShell`, `Container`, `Box`, `Paper`. **Layout:** Se usan los componentes de layout de Mantine: `Group`, `Stack`, `Grid`, `Flex`, `SimpleGrid`, `AppShell`, `Container`, `Box`, `Paper`.
**AppShell.Navbar / AppShell.Aside (gotchas v9):**
- NO override `position` via `style` (ej. `style={{ position: "relative" }}`). Mantine aplica `position: fixed` con CSS class; si lo pisas, el slot cae al flow normal y empuja el resto del layout abajo (root altura 2x).
- Para anclar children `position: absolute` (drag handle, badge flotante), el `position: fixed` del propio slot ya actua como containing block — no necesitas relative.
- Por defecto el navbar **empuja** el main (anade `padding-inline-start: navbar-width`). Para **overlay** (navbar tapa main):
```tsx
<AppShell styles={{ main: { paddingInlineStart: 0 } }}>
```
Idem `paddingInlineEnd: 0` para aside overlay.
- Si quieres backdrop dimming + click-outside-close: usa `<Drawer position="left">` en lugar de `AppShell.Navbar`.
- **Memoizar configs**: `header`/`navbar`/`aside`/`styles` aceptan objetos. Si el componente padre se re-renderiza cada N (tick, ws, etc.), los objetos literales se recrean y Mantine regenera el `<style>` inline. Wrap con `useMemo([deps])`:
```tsx
const navbarCfg = useMemo(() => ({ width, breakpoint: "md", collapsed: { ... } }), [width, navOpen]);
<AppShell navbar={navbarCfg} ...>
```
@@ -0,0 +1,115 @@
## Function growth + self-documenting capability
Dos doctrinas hermanas. Una define **como deben ser** las funciones (auto-descubribles y lanzables sin segunda lectura). La otra define **como crece** el registry (no inflando funciones — promoviendo composiciones a pipelines).
Issue 0087.
---
### Parte A — `.md` autosuficiente (contrato OBLIGATORIO)
Cuando Claude (o un humano) encuentra una funcion via FTS / fuzzy match / capability page / TOP block, el `.md` debe bastar para **lanzarla sin abrir el codigo**. Esto es lo que hace que descubrir = lanzar y elimina el coste del second lookup.
**Secciones obligatorias** en cada `.md` del registry (functions + pipelines + types con uso practico):
| Seccion | Contenido | Tamaño |
|---|---|---|
| Frontmatter | `name`, `signature`, `params` (con `desc` por param), `output`, `tags`, `uses_functions`, etc. Lo de hoy. | — |
| `## Ejemplo` | Bloque de codigo lanzable con args **concretos**. Copiar+pegar produce ejecucion real. NO placeholders abstractos. | 3-10 lineas |
| `## Cuando usarla` | 1-2 frases con triggers: "cuando hagas X / antes de Y / si necesitas Z". Verbos imperativos. Ayuda al fuzzy match y a Claude a saber sin leer el codigo. | 1-3 lineas |
| `## Gotchas` | Problemas conocidos / no-go cases. Obligatoria para funciones impuras o con efectos (Windows-side, red, FS write, GPU). Omisible para funciones puras triviales. | 0-5 puntos |
| `## Capability growth log` | Solo SI la funcion ha crecido. Una linea por version: `v1.1.0 (YYYY-MM-DD) — anade --build flag para skip build`. No se rellena en v1.0.0. | crece con el tiempo |
**Anti-patrones del .md:**
- Ejemplo con `<arg1>`, `<arg2>` placeholders abstractos — NO. Ejemplos con valores reales (`registry_dashboard`, `/home/lucas/...`).
- "Cuando usarla" vacio o "ver descripcion arriba" — NO. Frase nueva con trigger explicito.
- `notes` lleno + `## Gotchas` vacio cuando la funcion tiene efectos — mover de `notes` a `## Gotchas`.
- Capability growth log inventado (sin que la funcion haya cambiado) — NO. Solo se rellena cuando hay version bump real.
**Verificacion** (TBD: convertir a check de `fn doctor`): cada .md de `functions/`/`pipelines/` debe tener `## Ejemplo` y `## Cuando usarla`. `## Gotchas` obligatoria solo si `purity: impure`. `## Capability growth log` libre.
---
### Parte B — Crecimiento por composicion (no por inflado)
**Principio:** una funcion que hace bien UNA cosa NO necesita crecer. Anadir params "por si acaso" la hace peor (Inner Platform Effect). Lo que crece es el **registry**: pipelines nuevos que componen funciones existentes.
#### Ejemplo del principio
- **Hoy:** Claude para hacer una transferencia bancaria llama `bank_login` -> `bank_list_accounts` -> `bank_make_transfer`. 3 calls, 3 decisiones, 3 puntos de fallo.
- **Manana:** pipeline `bank_transfer_oneshot(account, amount, target)` que compone las 3 internamente. 1 call, 1 decision.
Misma capacidad, 3x menos pasos. **Esto es lo que multiplica la velocidad de Claude**, no anadir flags a `bank_login`.
#### Como se promueve una composicion
Senal detectable en `call_monitor.operations.db`: secuencia A→B(→C) con
- **Mismo session_id**.
- **Intervalo entre calls < N segundos** (default 30s).
- **Occurrences > K** (default 5) en ventana de **D dias** (default 30).
- **Success rate > S** (default 0.9 — falla < 10%).
- **No existe ya un pipeline** que la cubra (validar con FTS sobre `uses_functions`).
Cuando se cumple → **proposal `new_pipeline`** con evidencia (sequence_ids, session_ids, occurrence count). Humano (o `fn-orquestador` autonomo) decide promover.
#### Implementacion (issue 0087 tanda A)
- `call_monitor sequences --detect` subcomando: escanea `calls` table, agrupa por session+window, computa secuencias, upserta en tabla `function_sequences`.
- Cron diario que ejecuta el detector + genera proposals automaticas.
- Visible en Monitor tab del `registry_dashboard`: sub-tab "Promotion candidates".
#### Cuando SI inflar una funcion
Casos legitimos para anadir feature a una funcion existente:
1. **Generalizar firma** sin romper consumidores (anadir param opcional con default sensato).
2. **Mejor manejo de error** (mensajes mas claros, retry sensible).
3. **Default mas inteligente** (autodetectar lo que antes era arg obligatorio).
4. **Eliminar gotcha conocido** (fix de bug que estaba en `## Gotchas`).
NO infles para casos hipoteticos. NO anadas params "por flexibilidad". Si dudas, separa la responsabilidad en una funcion nueva o un pipeline.
#### Capability growth log — cuando se rellena
- Se rellena **solo cuando la funcion crece** (alguno de los 4 casos arriba).
- Cada bump de `version` -> 1 linea en `## Capability growth log` con fecha y resumen 1-frase.
- Una funcion estable de hace 6 meses puede seguir en v1.0.0 sin log: indica madurez, no abandono.
- Telemetria (call_monitor) decide si una funcion estable es huerfana (`calls_90d=0`) o usada-y-buena (`calls_30d>10, error_rate<0.05`). Las primeras se deprecan; las segundas se respetan.
---
### Parte C — Output de discovery
Cuando un mecanismo de discovery (fuzzy match / FRESH hook / TOP block / capability page) surfacea una funcion, el payload **minimo** es:
```
<id> → <signature> → <ejemplo de 1 linea>
```
Ejemplo concreto:
```
redeploy_cpp_app_windows_bash_pipelines
./fn run redeploy_cpp_app_windows registry_dashboard /path/to/app [--build]
use: tras compilar cpp/build/windows, antes de smoke test manual
```
Si Claude necesita mas (gotchas, params completos, codigo), un `mcp__registry__fn_show <id>` adicional. Pero el primer hit ya basta para el 80% de casos.
---
### Parte D — Relacion con otras reglas
- [[registry_first]] dice CUANDO buscar/usar/delegar. Esta regla dice **COMO** debe ser la funcion para que esa busqueda valga.
- [[ids_naming]] hace ID predictible. Esta regla hace metadata predictible.
- [[delegation]] dice cuando spawnar fn-constructor. Esta regla es lo que fn-constructor debe producir.
- [[capability_groups]] agrupa funciones afines. Las paginas madre de cada grupo deben respetar el mismo contrato self-doc (mejor con su propio ejemplo end-to-end por grupo).
### Resumen TL;DR
1. Cada `.md` autosuficiente: Ejemplo + Cuando usarla + Gotchas (si impura) + Growth log (si crecio).
2. Las funciones que hacen bien una cosa NO necesitan crecer.
3. El registry crece **promoviendo composiciones repetidas a pipelines**, no inflando funciones.
4. Telemetria de `call_monitor` detecta secuencias candidatas y abre proposals automaticas.
5. Discovery devuelve siempre: `id + signature + 1-line example`. Resto on-demand.
+58
View File
@@ -0,0 +1,58 @@
## Playgrounds: prototipos rapidos dentro de un artefacto
Un **playground** es un mini-artefacto efimero que vive **dentro** de otro artefacto (analysis, app o project) y reutiliza su entorno. Sirve para probar visualmente una idea (webapp, demo, dashboard, ejercicio interactivo) antes de decidir si se promueve a app independiente.
Ejemplo canonico: `projects/osint_graph/analysis/gliner_glirel_tuning/playground/` — server FastAPI + index.html + JS vendored que reutiliza el `.venv` del analisis padre para visualizar las recetas del notebook 08 con UI interactiva.
### Estructura
```
<artefacto_padre>/
playground/ # Un solo playground por padre (si necesitas mas, usa subdirs)
server.py | server.go | ... # Punto de entrada (single-file preferido)
index.html # UI si la hay
static/ # JS/CSS vendored (no node_modules ni pnpm)
server.log # Log local (gitignorable)
```
Si el playground crece a varios subdirs/modulos, ya no es playground — promover a app.
### Reglas
1. **Hereda el entorno del padre**. NO crea su propio `.venv`, `package.json`, ni dependencias. Si el padre es un analysis Python, usa `../.venv/bin/python3`. Si el padre es una app Go, comparte el `go.mod`.
2. **NO se indexa**. No tiene `app.md`, no aparece en `registry.db`, no tiene entrada en `pc_locations`.
3. **NO tiene repo propio**. Vive dentro del repo Gitea del artefacto padre y se mueve con el.
4. **Single-file preferido**. Un `server.py` o `main.go` con todo dentro. Si hace falta partir, considera promover a app.
5. **Vendor deps front**. JS/CSS como `.min.js` en `static/`, sin `node_modules`. Si necesitas pnpm/vite, ya no es playground.
6. **Reutiliza funciones del registry** igual que el padre — `sys.path` al `python/functions`, importar paquetes, etc.
7. **Ciclo de vida**: vive mientras la idea esta cruda. Una vez probada, dos caminos:
- **Promover a app** (extraer logica reutilizable como funciones del registry, crear `app.md`, mover a `apps/`).
- **Borrar** sin contemplaciones si el experimento no llevo a nada.
### Cuando NO usar playground
- Si necesitas correr en un VPS / tener systemd / health check → es un app + service, no playground.
- Si la idea ya esta clara y el codigo va a sobrevivir meses → arrancar como app desde el primer dia ahorra una migracion.
- Si necesitas operations.db, assertions, o el bucle reactivo → es app.
- Si el padre seria un proyecto entero solo para contener el playground → probablemente sea app standalone con `tags: [prototype]`.
### Relacion con `temp/`
| Cuando | Donde |
|---|---|
| Idea suelta sin contexto, prueba de API, snippet desechable | `temp/<lo_que_sea>/` (gitignored, sin contexto) |
| Prototipo ligado a un analysis/app/proyecto que reutiliza su entorno | `<padre>/playground/` (versionado con el padre) |
| Codigo que sobrevive y se reutiliza en otros sitios | extraer a `functions/` |
| Aplicacion ejecutable con identidad propia | `apps/` o `projects/<p>/apps/<a>/` |
`temp/` es para cosas sin padre. Playground es para cosas con padre. Si dudas entre los dos, empieza en `temp/` y mueve a `playground/` cuando quede claro de que artefacto depende.
### Lanzar un playground
Sin convencion fija — depende del stack. El propio `server.py` o un README en el playground documenta como arrancarlo. Ejemplo del playground de OSINT:
```bash
cd projects/osint_graph/analysis/gliner_glirel_tuning/playground
../.venv/bin/python3 server.py
# http://localhost:7878
```
+147
View File
@@ -0,0 +1,147 @@
## Como invocar funciones del registry — patrones canonicos
Toda invocacion del agente al registry sigue uno de **tres patrones**. Cualquier otro patron es antipatron auditable. Las invocaciones se loguean en `projects/fn_monitoring/apps/call_monitor/operations.db` (issue 0085) para alimentar el bucle reactivo.
### Patrones canonicos
| Caso | Patron | Cuando |
|---|---|---|
| **Inspeccionar** (buscar, leer codigo, ver dependencias, listar dominios, leer proposals) | `mcp__registry__fn_search` / `fn_show` / `fn_code` / `fn_uses` / `fn_list_domains` / `fn_proposal` | SIEMPRE para descubrimiento, lectura de codigo, exploracion. |
| **Ejecutar** UNA funcion/pipeline con sus args | `mcp__registry__fn_run <id> [args]` (preferido) o `./fn run <id> [args]` (fallback CLI) | ID conocido + args planos. Despacho automatico por lenguaje. |
| **Componer** ad-hoc multi-funcion con logica intermedia | Heredoc `python/.venv/bin/python3 - <<'PYEOF' ... PYEOF` IMPORTANDO funciones del registry | Solo si hay loops/conditionals/dispatch entre N funciones. Las funciones del registry **se importan**, no se reescriben. |
### Antipatrones prohibidos (audit-targeted)
| Patron | Razon | Sustituir por |
|---|---|---|
| `sqlite3 registry.db "SELECT ..."` para buscar funciones/tipos | Salta MCP, FTS5 gotchas, sin trazabilidad | `mcp__registry__fn_search` |
| `sqlite3 registry.db "SELECT ... FROM proposals"` | Mismo problema | `mcp__registry__fn_proposal` |
| `python -c "import metabase; dir(metabase)"` para descubrir helpers | Fuente de verdad = registry, no `__init__.py` | `mcp__registry__fn_search "metabase"` + `mcp__registry__fn_show <id>` |
| Heredoc que reescribe logica que ya existe como funcion del registry | Reinvento + perdida de capitalizacion | Buscar primero; si falta, delegar a `fn-constructor` (no escribir inline) |
| `client._http.request(...)` saltando wrapper del registry | Salta validacion del wrapper y telemetria | Usar wrapper; si firma incompleta, `fn proposal add --kind improve_function` |
| Scripts en `temp/` para composiciones que se repiten >2 veces | Codigo perdido + sin monitoreo | Pipeline en `python/functions/pipelines/` o `bash/functions/pipelines/` |
| `from <pkg> import *` en heredoc | Imposible identificar funciones usadas | Imports explicitos `from <domain> import <name1>, <name2>` |
### Excepciones autorizadas para `sqlite3` directo
Casos donde el MCP no aplica y `sqlite3 registry.db` es legitimo:
- Introspeccion de schema: `.schema`, `.tables`, `PRAGMA table_info(...)`, `PRAGMA index_list(...)`.
- Agregaciones: `COUNT(*)`, `GROUP BY`, `SUM(...)`, `AVG(...)`.
- JOINs custom entre tablas que el MCP no expone (`functions JOIN unit_tests ON ...`).
- Columnas que el MCP no devuelve (rare; preferir proponer ampliacion del MCP).
El hook `PreToolUse` (`.claude/scripts/hook_registry_mcp.sh`) ya deja pasar estas excepciones y solo avisa cuando ve `sqlite3 registry.db "SELECT ..."` plano.
### Excepcion: hooks e infraestructura de telemetria (issue 0087)
Los **hooks** (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, etc.) y los **binarios de infraestructura** que sirven al agente (`fn_match`, `fn doctor`, `call_monitor`) **pueden leer `registry.db` directo** via `sqlite3` o `database/sql` con conexion read-only. NO estan sujetos a la regla MCP-first porque:
- No son acciones del agente — son inspeccion automatizada del entorno.
- El MCP requiere tool invocation por Claude; un hook no puede invocar tools.
- Latencia objetivo (50-200ms) incompatible con round-trip MCP.
**Restricciones:**
- SOLO lectura. Conexion debe abrirse con `?mode=ro` o `?_query_only=1`.
- NUNCA escritura a `registry.db` desde hooks.
- Si un hook necesita escribir (cache, telemetria propia), usa su propia DB (`operations.db` del app de hooks, o `~/.fn_hooks/cache.db`).
Esta excepcion es **explicita y acotada** — no aplica al agente, que sigue regido por la regla MCP-first.
### Verificacion previa — `fn doctor`
Antes de empezar trabajo no trivial sobre el registry, ejecutar `fn doctor` para confirmar que el ecosistema esta sano:
- Artefactos OK (sin `git_not_initialized`, `venv_broken_path`, etc.).
- Services activos cuando se necesiten (`sqlite_api`, `registry_api`, `registry_mcp`).
- Sin drift `pc_locations` vs disco.
- Sin drift `uses_functions` vs imports reales.
Si `fn doctor` reporta `service inactive` para `registry_mcp.service`, el MCP estara siendo invocado en modo stdio por Claude Code (normal); el systemd unit solo aplica al modo HTTP. Si el binario no responde, rebuild: `cd apps/registry_mcp && CGO_ENABLED=1 go build -tags fts5 -o registry_mcp .`.
### Tools MCP disponibles
| Tool | Lectura/escritura | Gating |
|---|---|---|
| `fn_search` | read | siempre on |
| `fn_show` | read | siempre on |
| `fn_code` | read | siempre on |
| `fn_uses` | read | siempre on |
| `fn_list_domains` | read | siempre on |
| `fn_proposal` | read | siempre on |
| `fn_doctor` | read | siempre on |
| `fn_run` | execute (mutating side-effects) | requiere `--enable-run` |
| `fn_create_function` | write | requiere `--enable-write` |
### Heredoc Python — convenciones obligatorias
Cuando el caso 3 (composicion) sea inevitable:
1. **Imports explicitos** desde paquetes del registry. Nunca `import *`.
2. **No reescribir** la firma de una funcion del registry — importarla.
3. **Args via env vars o stdin JSON**, nunca interpolacion shell directa (inyeccion).
4. **Output a stdout JSON** cuando vaya a ser consumido por el siguiente paso.
5. **Si el heredoc supera ~30 lineas**, extraer a `python/functions/pipelines/`. El monitor avisara automaticamente cuando un patron similar se repita >5 veces.
### Trazabilidad — bucle reactivo
Cada evento alimenta a `call_monitor.db` (event-log append-only) y se rollupea en una vista `function_stats` con contadores por funcion del registry. Tablas event-log:
| Tabla | Captura |
|---|---|
| `calls` | Cada invocacion (heredoc/mcp/fn_run): function_id, tool_used, duration_ms, success, error_class, args_hash |
| `code_writes` | Cada Edit/Write sobre archivo del registry: function_id, session_id, lines_added/removed |
| `test_runs` | Cada `go test`/`pytest` que toca codigo del registry: function_id, test_id, passed, duration_ms |
| `e2e_runs_fn` | Cada check `e2e_checks` de app que usa la funcion: function_id, app_id, check_id, passed |
| `violations` | Antipatron detectado: rule_id, session_id, command_snippet, severity |
| `patterns` | Heredocs clusterizados: pattern_hash, session_ids[], occurrences, representative_snippet |
| `sessions` | session_id, cwd, started_at, ended_at, health_score, mcp_ratio |
Vista agregada `function_stats` por `function_id`:
- **Uso:** `calls_total`, `calls_24h/7d/30d/90d`, `last_used_at`
- **Errores:** `errors_total`, `error_rate`, `last_error_class`, `last_error_ts`
- **Performance:** `mean_duration_ms`, `p95_duration_ms`
- **Codigo:** `writes_count`, `last_write_at`
- **Tests:** `tests_total`, `tests_failed`, `test_fail_rate`, `last_test_failed_at`
- **E2E:** `e2e_total`, `e2e_failed`, `e2e_fail_rate`, `consumer_apps_count`
- **Salud:** `violations_caused`
Assertions derivadas → proposals automaticas:
| Regla | Threshold | Proposal |
|---|---|---|
| Huerfana absoluta | `calls_90d=0 AND writes_count=0` | `deprecate_function` |
| Bug prioritario | `error_rate>0.1 AND calls_7d>5` | `improve_function` (bug) |
| Regresion performance | `p95_24h > 1.5 * p95_30d` | `improve_function` (perf) |
| Test flaky | `test_fail_rate>0.1 AND tests_total>10` | `improve_function` (flaky) |
| Wrapper saltado | `violations_caused>3` | `improve_function` (API gap) |
| Patron inline sin funcion | `patterns.occurrences>5 AND no match FTS` | `new_function` con snippet |
| Blast radius alto | `e2e_fail_rate>0 AND consumer_apps_count>=3` | `improve_function` (critical) |
Datos sensibles: solo `args_hash`, NUNCA valores concretos. Snippets de error redactados via allowlist.
### Capas de monitorizacion (issue 0085)
Cobertura por capa, no todas activas a la vez:
| # | Capa | Activacion | Cobertura |
|---|---|---|---|
| 1 | Hook PostToolUse Bash | siempre (settings.local.json) | mcp, fn_cli_run, edit_registry, violations |
| 2 | Wrapper Python `registry_telemetry` | `FN_TELEMETRY=1` env var | heredocs + notebooks Jupyter |
| 3 | Wrapper Bash `telemetry_prelude.sh` | `source` explicito o `FN_TELEMETRY=1` | heredoc bash + apps bash |
| 4 | Interceptor en `fn run` | siempre (binario Go) | duration/error real de invocacion CLI |
| 5 | `fn doctor copied-code` | comando manual / cron | drift estatico: codigo copiado en apps |
| 6 | `function_versions` + snapshot | poblado por `fn index` + edit-hook | historial de versiones |
| 7-8 | Build-tag Go / macro C++ | opt-in por app | runtime de app (futuro) |
**Boundary:** monitorizamos al **agente** y a **invocaciones canonicas**. Runtime de apps Go/C++ compiladas queda fuera. Compensar con tests + `e2e_checks` (issue 0068).
### Que NO se monitoriza
- Funcion Go/C++ llamada internamente por app ya compilada.
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
- Service de produccion recibiendo HTTP.
**Implicacion:** una funcion con `calls_90d=0` puede ser huerfana real O usada en runtime invisible. Antes de proponer `deprecate_function`, cruzar con `consumer_apps_count > 0` (e2e) o con `fn doctor uses-functions` (declaraciones estaticas).
+50
View File
@@ -0,0 +1,50 @@
## Registry-first: reutilizar antes que escribir, delegar antes que escribir inline
**OBLIGATORIO para todos los artefactos** (apps, analyses, projects, playgrounds, services). El registry existe para que las apps se compongan a partir de funciones probadas. No respetar esto convierte cada app en una isla con codigo duplicado y bugs unicos.
### Flujo obligatorio antes de escribir codigo en un artefacto
1. **Consultar registry.db con FTS5** para encontrar funciones existentes que cubran el caso. No es opcional. Buscar por `name`, `description`, `tags`, `signature`, `code` y `params_schema`. Probar varios sinonimos (`http`, `serve`, `router`; `id`, `uuid`, `random_hex`; etc.).
2. **Reutilizar lo que existe**. Importar la funcion del registry y declararla en `uses_functions` del `app.md`. NO reescribir logica inline cuando ya hay una funcion.
3. **Si falta una pieza reutilizable → delegar a `fn-constructor`** (subagent_type `fn-constructor`). NO escribir la funcion inline en el artefacto. El agente construye la funcion en su sitio (`functions/{domain}/`, `python/functions/{domain}/`, etc.) con `.go/.py/.sh/.ts` + `.md` correctos, tests, y respetando las reglas de pureza/firma.
4. **Solo despues** se escribe el codigo del artefacto, que orquesta funciones del registry y aporta unicamente la logica especifica del dominio (CRUD de tablas concretas, layout de UI, flujo de la app).
### Que va al registry vs que va al artefacto
| Tipo de codigo | Donde |
|---|---|
| Logica reutilizable, primitiva, generica (parser, helper http, abrir SQLite, generar IDs, formatear timestamps con politica fija, middleware, etc.) | `functions/{domain}/` — delegar a `fn-constructor` si no existe |
| Composicion de varias funciones del registry para un flujo concreto | `pipelines` (registry) o codigo del artefacto segun reusabilidad |
| Schema SQL especifico del artefacto | Migraciones del artefacto |
| Handlers HTTP que solo hacen sentido para este artefacto (ej. `/api/board` de un kanban) | Codigo del artefacto, pero usando `http_json_response_go_infra`, `http_parse_body_go_infra`, etc. del registry |
| Layout/components especificos de la UI del artefacto | Codigo del artefacto, pero consumiendo componentes de `frontend/functions/ui/` (`@fn_library`) |
Regla practica: **si dos artefactos ya hacen o haran lo mismo, es funcion del registry**. One-liners idiomaticos de la stdlib (`time.Now().UTC().Format(...)`) NO necesitan ser registry — se ven en cualquier sitio. Pero un patron como "abrir SQLite con WAL + foreign keys + ping" SI (y por eso existe `sqlite_open_go_infra`).
### Cuando delegar a `fn-constructor`
Delegar SIEMPRE que se necesite una funcion reutilizable que no existe. El prompt del subagente debe incluir:
- Lenguaje, dominio, nombre propuesto.
- Firma esperada (params + return).
- Pureza (`pure` o `impure`).
- Una breve descripcion del proposito y del comportamiento.
- Si hay funciones similares en el registry, listarlas para evitar duplicados.
El agente construye la funcion siguiendo las reglas del registry (`purity.md`, `ids_naming.md`, `types_in_signatures.md`, etc.) y deja `fn index` listo para ejecutar.
### Auditoria
Despues de implementar el artefacto, verificar que `uses_functions` del `app.md` (o equivalente) declara TODAS las funciones del registry consumidas. Esto se puede cruzar con los `import` reales del codigo:
```bash
# Para Go:
grep -rh '"fn-registry/functions/' apps/<app>/ | sort -u
# Cada paquete importado tiene que tener al menos una funcion declarada en uses_functions.
```
### Por que esta regla
Sin esta regla cada app reinventa: helpers SQLite, middleware HTTP, generacion de IDs, parsers, validadores, formateo de fechas. El registry pierde su razon de ser. Con esta regla, una funcion bien hecha se reutiliza en N apps; un bug se arregla una vez; la velocidad de cada app nueva crece a medida que el registry crece.
+35
View File
@@ -0,0 +1,35 @@
## uses_functions
Cuando un .cpp llama a otra funcion del registry, el `.md` del CONSUMIDOR
debe anadir la dependencia a `uses_functions`. El indexer NO lo deduce
automaticamente para C++ (parser no trivial).
Como auditar (funciones huerfanas):
sqlite3 registry.db "SELECT id FROM functions WHERE lang='cpp' AND uses_functions='[]';"
Como auditar (drift entre `CMakeLists.txt` y `app.md`):
- Cruzar los `${CMAKE_SOURCE_DIR}/functions/<dom>/<name>.cpp` listados en el
`CMakeLists.txt` con el `uses_functions` del `app.md`. Cada `.cpp` linkado
debe aparecer como `<name>_cpp_<dom>` en el `.md`. Excepciones: ver mas abajo.
Convencion:
- **Framework code** (`cpp/framework/app_base.cpp`) — no esta indexado.
- **Funciones bundled en `fn_framework`** — son funciones del registry cuyo
`.cpp` se compila dentro del static lib `fn_framework` (lista en
`cpp/CMakeLists.txt`, target `add_library(fn_framework STATIC ...)`):
`tokens`, `icon_font`, `app_settings`, `app_about`, `fps_overlay`,
`panel_menu`, `app_menubar`, `layouts_menu`, `logger`, `log_window`,
`gl_loader`, `layout_storage`, `selectable_text`. Las apps las usan
transitivamente (incluyen `core/logger.h`, llaman `fn_log::log_info`),
pero NO listan estos `.cpp` en su `CMakeLists.txt` (multiple-definition)
ni los declaran en `uses_functions` del `app.md`. Excepcion: si una app
toca una API que no este en fn_framework (raro), declara la dep.
- **TU adicional de un parent function** (ej. `graph_labels_select.cpp` que
va con `graph_labels.cpp`) — desde 2026-05-04 se registra como entrada
propia con su `.md` (ver ADR 0003). El parent declara la nueva entrada
en su `uses_functions`. Las apps que enlazan ambos `.cpp` listan ambas
IDs en `uses_functions` del `app.md`.
- **Apps** (`apps/`, `cpp/apps/`, `projects/*/apps/`) son leaves del grafo:
declaran `uses_functions` en `app.md` pero ninguna funcion del registry
las cita.
- DEMO_ONLY en `primitives_gallery` se etiqueta `notes: scaffolding/demo`.
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env bash
# PostToolUse hook: registra cada invocacion del agente en
# projects/fn_monitoring/apps/call_monitor/operations.db (issue 0085b).
#
# Identifica tool, extrae function_id cuando es posible, clasifica el patron
# (mcp_*, fn_cli_run, heredoc_py, sqlite_direct, edit_registry, ...) y
# detecta antipatrones para registrar violations.
#
# NUNCA bloquea la herramienta. Falla silenciosamente si la BD no esta lista.
# Solo guarda args_hash, jamas valores concretos.
set -euo pipefail
# ---- Resolve registry root (walks up from cwd looking for registry.db) ----
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
DB="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
# Si la BD aun no existe, el hook no hace nada (esperando init).
[ -f "$DB" ] || exit 0
# ---- Read stdin JSON ----
INPUT=$(cat)
if [ -z "$INPUT" ]; then exit 0; fi
# Required jq presence
command -v jq >/dev/null 2>&1 || exit 0
command -v sqlite3 >/dev/null 2>&1 || exit 0
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""')
TS=$(date -u +%s)
# Tool response success/error
SUCCESS=1
ERROR_CLASS=""
ERROR_SNIPPET=""
RESP_IS_ERROR=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.is_error // false) else false end')
if [ "$RESP_IS_ERROR" = "true" ]; then
SUCCESS=0
ERROR_SNIPPET=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.error // .tool_response.content // "") else "" end' | head -c 240 | tr '\n' ' ')
fi
# args_hash: sha256 truncado del tool_input (sin valores)
ARGS_HASH=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' | sha256sum | cut -c1-16)
# Helpers SQL
sql_escape() { printf '%s' "$1" | sed "s/'/''/g"; }
insert_call() {
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}" snippet="${4:-}"
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc snip_esc
# Politica issue 0087: command_snippet solo se rellena cuando function_id
# esta vacio. Si la call golpea una funcion del registry, su ID y
# tool_used bastan; no duplicamos el comando.
if [ -n "$fn_id" ]; then snippet=""; fi
# Redact common secrets antes de persistir
snippet=$(printf '%s' "$snippet" \
| sed -E 's/(password|token|secret|api[_-]?key|bearer)([[:space:]]*[=:][[:space:]]*)[^[:space:]]+/\1\2<REDACTED>/Ig' \
| head -c 200)
fn_esc=$(sql_escape "$fn_id")
tu_esc=$(sql_escape "$tool_used")
ec_esc=$(sql_escape "$ERROR_CLASS")
es_esc=$(sql_escape "$ERROR_SNIPPET")
sid_esc=$(sql_escape "$SESSION_ID")
ah_esc=$(sql_escape "$ARGS_HASH")
snip_esc=$(sql_escape "$snippet")
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, command_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc','$snip_esc',$TS);" 2>/dev/null || true
}
insert_code_write() {
local fn_id="$1" file_path="$2" added="${3:-0}" removed="${4:-0}"
local fn_esc fp_esc sid_esc
fn_esc=$(sql_escape "$fn_id")
fp_esc=$(sql_escape "$file_path")
sid_esc=$(sql_escape "$SESSION_ID")
sqlite3 "$DB" "INSERT INTO code_writes (session_id, function_id, file_path, lines_added, lines_removed, ts) VALUES ('$sid_esc','$fn_esc','$fp_esc',$added,$removed,$TS);" 2>/dev/null || true
}
# Snapshot a function version row when an edit lands on a registry file.
# Uses sha256 of file bytes as content_hash (separate namespace from index source).
insert_edit_version() {
local fn_id="$1" abs_path="$2"
[ -f "$abs_path" ] || return 0
command -v sha256sum >/dev/null 2>&1 || return 0
local hash
hash=$(sha256sum "$abs_path" 2>/dev/null | awk '{print $1}')
[ -z "$hash" ] && return 0
local fn_esc h_esc
fn_esc=$(sql_escape "$fn_id")
h_esc=$(sql_escape "$hash")
sqlite3 "$DB" "INSERT OR IGNORE INTO function_versions (function_id, content_hash, version, snapped_at, source, lines_added, lines_removed) VALUES ('$fn_esc','$h_esc','',$TS,'edit_hook',0,0);" 2>/dev/null || true
}
insert_violation() {
local rule_id="$1" fn_id="$2" snippet="$3" severity="${4:-warning}"
local r_esc fn_esc sn_esc sev_esc sid_esc
r_esc=$(sql_escape "$rule_id")
fn_esc=$(sql_escape "$fn_id")
sn_esc=$(sql_escape "$(printf '%s' "$snippet" | head -c 240 | tr '\n' ' ')")
sev_esc=$(sql_escape "$severity")
sid_esc=$(sql_escape "$SESSION_ID")
sqlite3 "$DB" "INSERT INTO violations (session_id, rule_id, function_id, command_snippet, severity, ts) VALUES ('$sid_esc','$r_esc','$fn_esc','$sn_esc','$sev_esc',$TS);" 2>/dev/null || true
}
# ---- Derive function_id from registry file path ----
# Matches paths under functions/<domain>/<name>.<ext>, python/functions/<domain>/<name>.py,
# bash/functions/<domain>/<name>.sh, frontend/functions/<domain>/<name>.ts(x)
derive_fn_id_from_path() {
local p="$1"
[ -z "$p" ] && return 1
case "$p" in
functions/*/*.go|*/functions/*/*.go)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|.*functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|.*functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_go_%s' "$name" "$dom" && return 0 ;;
python/functions/*/*.py)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|python/functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|python/functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_py_%s' "$name" "$dom" && return 0 ;;
bash/functions/*/*.sh)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|bash/functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|bash/functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_bash_%s' "$name" "$dom" && return 0 ;;
frontend/functions/*/*.ts|frontend/functions/*/*.tsx)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|frontend/functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|frontend/functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_ts_%s' "$name" "$dom" && return 0 ;;
esac
return 1
}
# ---- Dispatch by tool ----
case "$TOOL_NAME" in
mcp__registry__fn_search)
insert_call "" "mcp_fn_search"
;;
mcp__registry__fn_show)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_show"
;;
mcp__registry__fn_code)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_code"
;;
mcp__registry__fn_uses)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_uses"
;;
mcp__registry__fn_run)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_run"
;;
mcp__registry__fn_list_domains)
insert_call "" "mcp_fn_list_domains"
;;
mcp__registry__fn_proposal)
insert_call "" "mcp_fn_proposal"
;;
mcp__registry__fn_doctor)
insert_call "" "mcp_fn_doctor"
;;
mcp__registry__fn_create_function)
insert_call "" "mcp_fn_create_function"
;;
Edit|Write|MultiEdit)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')
ABS_PATH="$FILE_PATH"
# Make path relative to root if absolute and inside root
case "$FILE_PATH" in
"$ROOT"/*) FILE_PATH="${FILE_PATH#$ROOT/}" ;;
/*) ABS_PATH="$FILE_PATH" ;;
*) ABS_PATH="$ROOT/$FILE_PATH" ;;
esac
FN_ID=$(derive_fn_id_from_path "$FILE_PATH" || true)
if [ -n "$FN_ID" ]; then
insert_code_write "$FN_ID" "$FILE_PATH" 0 0
insert_call "$FN_ID" "edit_registry"
insert_edit_version "$FN_ID" "$ABS_PATH"
fi
;;
Bash)
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""')
CMD_HEAD=$(printf '%s' "$CMD" | head -c 200 | tr '\n' ' ')
# Classify
TOOL_USED="bash_other"
FN_ID=""
if printf '%s' "$CMD" | grep -qE '(^|[[:space:]])\./fn[[:space:]]+run[[:space:]]+'; then
TOOL_USED="fn_cli_run"
FN_ID=$(printf '%s' "$CMD" | sed -nE 's/.*\.\/fn[[:space:]]+run[[:space:]]+([A-Za-z0-9_]+).*/\1/p' | head -n1)
elif printf '%s' "$CMD" | grep -qE 'python/\.venv/bin/python3[[:space:]]+-[[:space:]]+<<'; then
TOOL_USED="heredoc_py"
elif printf '%s' "$CMD" | grep -qE 'sqlite3[[:space:]][^|]*\bregistry\.db\b'; then
TOOL_USED="sqlite_direct"
fi
insert_call "$FN_ID" "$TOOL_USED" 0 "$CMD_HEAD"
# ---- Violation rules ----
# 1. sqlite3 directo SELECT sobre registry.db (excepto schema/pragma/count/join)
if [ "$TOOL_USED" = "sqlite_direct" ]; then
if ! printf '%s' "$CMD" | grep -qiE '(\.schema|\.tables|PRAGMA[[:space:]]+(table_info|index_list)|COUNT\(|GROUP[[:space:]]+BY|JOIN[[:space:]])'; then
insert_violation "sqlite3_registry_select" "" "$CMD_HEAD" "warning"
fi
fi
# 2. python -c "import X; dir(X)"
if printf '%s' "$CMD" | grep -qE 'python[3]?[[:space:]]+-c[[:space:]]+["'\''].*import.*(dir|help)\('; then
insert_violation "python_dir_inspect" "" "$CMD_HEAD" "info"
fi
# 3. from <pkg> import * (en heredoc python)
if [ "$TOOL_USED" = "heredoc_py" ]; then
if printf '%s' "$CMD" | grep -qE 'from[[:space:]]+[A-Za-z0-9_.]+[[:space:]]+import[[:space:]]+\*'; then
insert_violation "import_star_in_heredoc" "" "$CMD_HEAD" "warning"
fi
if printf '%s' "$CMD" | grep -qE 'client\._http\.request\('; then
insert_violation "client_http_request_direct" "" "$CMD_HEAD" "warning"
fi
fi
;;
esac
exit 0
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: inyecta capacidades calientes (TOP/FRESH/PIPELINES)
# del registry como additionalContext en cada turno del usuario.
#
# Cache: ~/.cache/fn_registry/capabilities.txt (TTL 1h).
# Fuente: `./fn doctor capabilities --emit-claude-md` desde la raiz del repo.
#
# NUNCA bloquea: si algo falla, emite contexto vacio y sale 0.
set -uo pipefail
CACHE_DIR="${HOME}/.cache/fn_registry"
CACHE_FILE="${CACHE_DIR}/capabilities.txt"
TTL_SECONDS=3600
# Resolve registry root (walks up from cwd, fallback CLAUDE_PROJECT_DIR)
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ] && [ -x "$d/fn" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "${CLAUDE_PROJECT_DIR}/registry.db" ]; then
printf '%s' "${CLAUDE_PROJECT_DIR}"
return 0
fi
return 1
}
# Consume stdin (UserPromptSubmit payload) — we don't need it but keep stdin clean
cat >/dev/null 2>&1 || true
ROOT=$(resolve_root) || exit 0
mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0
# Cache freshness check
need_refresh=1
if [ -f "$CACHE_FILE" ]; then
now=$(date +%s)
mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)
age=$((now - mtime))
if [ "$age" -lt "$TTL_SECONDS" ]; then
need_refresh=0
fi
fi
if [ "$need_refresh" -eq 1 ]; then
# Regenerate: call fn doctor capabilities --emit-claude-md and process
raw=$("$ROOT/fn" doctor capabilities --emit-claude-md 2>/dev/null || true)
if [ -z "$raw" ]; then
exit 0
fi
# Extract top 5 from each section using awk.
# Sections detected by "## ... Top" / "## ... Fresh" / "## ... Pipelines".
line=$(printf '%s\n' "$raw" | awk '
BEGIN { sec=""; n_top=0; n_fresh=0; n_pipe=0; }
/^## .*Top 20/ { sec="TOP"; next }
/^## .*Fresh/ { sec="FRESH"; next }
/^## .*Pipelines/ { sec="PIPE"; next }
/^## / { sec=""; next }
/^- `/ {
# extract first backticked token
s = $0
sub(/^- `/, "", s)
i = index(s, "`")
if (i == 0) next
id = substr(s, 1, i-1)
if (sec == "TOP" && n_top < 5) { tops[n_top++] = id }
if (sec == "FRESH" && n_fresh < 5) { fresh[n_fresh++] = id }
if (sec == "PIPE" && n_pipe < 5) { pipes[n_pipe++] = id }
}
END {
out = "CAPABILITIES (cache 1h):"
if (n_top > 0) {
line = " TOP: " tops[0]
for (i=1; i<n_top; i++) line = line ", " tops[i]
out = out "\n" line
}
if (n_fresh > 0) {
line = " FRESH (7d): " fresh[0]
for (i=1; i<n_fresh; i++) line = line ", " fresh[i]
out = out "\n" line
}
if (n_pipe > 0) {
line = " PIPELINES: " pipes[0]
for (i=1; i<n_pipe; i++) line = line ", " pipes[i]
out = out "\n" line
}
print out
}
')
if [ -z "$line" ]; then
exit 0
fi
printf '%s\n' "$line" >"$CACHE_FILE" 2>/dev/null || exit 0
fi
# Emit cached content as additionalContext
if [ ! -s "$CACHE_FILE" ]; then
exit 0
fi
ctx=$(cat "$CACHE_FILE")
if command -v jq >/dev/null 2>&1; then
jq -n --arg ctx "$ctx" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
else
# Fallback: print raw text (Claude Code prints stdout as context too)
printf '%s\n' "$ctx"
fi
exit 0
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# PostToolUse hook: gate "tag de capability group obligatorio" tras crear/modificar
# funciones del registry. Issue 0086 paso 9/gate.
#
# Comportamiento:
# - Detecta .md de funciones (functions/, python/functions/, bash/functions/,
# frontend/functions/, cpp/functions/) modificados en los ultimos 60s.
# - Lee frontmatter `tags:` y verifica si al menos uno coincide con un capability
# group declarado en docs/capabilities/INDEX.md.
# - Si NO hay match -> emite additionalContext con la lista de funciones afectadas.
# - NUNCA bloquea. Solo warning visible.
#
# Salida JSON consumida por Claude Code:
# { "hookSpecificOutput": { "hookEventName": "PostToolUse",
# "additionalContext": "..." } }
set -euo pipefail
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
INDEX="$ROOT/docs/capabilities/INDEX.md"
# Si no existe el INDEX aun, no hay grupos definidos -> nada que verificar.
[ -f "$INDEX" ] || exit 0
# Consume stdin (sin parsear — no necesitamos session_id para este gate)
cat >/dev/null
# Solo correr si hay jq disponible
command -v jq >/dev/null 2>&1 || exit 0
# 1. Cargar lista de capability groups desde el INDEX.
# Formato esperado en INDEX.md: | [name](name.md) | N | descripcion |
CAP_GROUPS=$(grep -oE '\[[a-z][a-z0-9_-]*\]\([a-z][a-z0-9_-]*\.md\)' "$INDEX" \
| sed -E 's/^\[([^]]+)\].*/\1/' \
| sort -u)
[ -z "$CAP_GROUPS" ] && exit 0
# 2. Encontrar .md de funciones modificados en ultimos 60s.
RECENT=$(find "$ROOT/functions" "$ROOT/python/functions" "$ROOT/bash/functions" \
"$ROOT/frontend/functions" "$ROOT/cpp/functions" \
-maxdepth 4 -type f -name '*.md' -mmin -1 2>/dev/null || true)
[ -z "$RECENT" ] && exit 0
# 3. Para cada .md reciente: extraer tags del frontmatter, comparar con groups.
MISSING=""
while IFS= read -r mdfile; do
[ -z "$mdfile" ] && continue
# Extrae el bloque entre los dos `---` del inicio
front=$(awk '/^---$/{c++; next} c==1 {print} c>=2 {exit}' "$mdfile" 2>/dev/null || true)
[ -z "$front" ] && continue
# tags: [a, b, c] o tags:\n - a\n - b
tags_inline=$( { printf '%s\n' "$front" | grep -E '^tags:[[:space:]]*\[' | head -1 \
| sed -E 's/^tags:[[:space:]]*\[(.*)\].*$/\1/' \
| tr ',' '\n' | sed -E 's/^[[:space:]"]+|[[:space:]"]+$//g'; } || true )
tags_block=$( { printf '%s\n' "$front" | awk '
/^tags:[[:space:]]*$/ {intag=1; next}
intag && /^[[:space:]]*-[[:space:]]/ {sub(/^[[:space:]]*-[[:space:]]*/, ""); print; next}
intag && !/^[[:space:]]/ {intag=0}
' | sed -E 's/^[[:space:]"]+|[[:space:]"]+$//g'; } || true )
tags=$( { printf '%s\n%s\n' "$tags_inline" "$tags_block" | grep -v '^$'; } || true )
matched=0
while IFS= read -r g; do
[ -z "$g" ] && continue
if printf '%s\n' "$tags" | grep -qx "$g"; then
matched=1
break
fi
done <<< "$CAP_GROUPS"
if [ "$matched" -eq 0 ]; then
rel="${mdfile#$ROOT/}"
MISSING="${MISSING}${rel}\n"
fi
done <<< "$RECENT"
# 4. Si hay funciones sin tag de grupo, emitir aviso.
if [ -n "$MISSING" ]; then
CAP_GROUPS_CSV=$(printf '%s' "$CAP_GROUPS" | tr '\n' ',' | sed 's/,$//')
WARN="CAPABILITY-GAP (issue 0086): funcion(es) recien tocada(s) sin tag de capability group: $(printf '%b' "$MISSING" | tr '\n' ' ')"
WARN+="| Grupos disponibles: ${CAP_GROUPS_CSV}. Anade al menos uno al frontmatter \`tags:\` y corre \`./fn index\`. Si la funcion no encaja en ningun grupo existente, considera crear grupo nuevo (>=3 funciones) o dejarla con tag plano (no de grupo)."
jq -n --arg ctx "$WARN" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}'
fi
exit 0
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# PreToolUse hook: sugiere funciones del registry cuando un comando Bash
# inline probablemente reinventa una funcion existente (issue 0087).
#
# Llama a `./fn match "<cmd>"` con timeout 200ms. Si encaja con alta
# confianza, imprime un <system-reminder> a stderr para que Claude Code
# lo lea como recordatorio. NUNCA bloquea la tool — exit 0 siempre.
set -euo pipefail
# ---- Always exit 0, no matter what ----
trap 'exit 0' ERR
# ---- Resolve registry root (walks up from cwd) ----
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
FN_BIN="$ROOT/fn"
[ -x "$FN_BIN" ] || exit 0
# ---- Read stdin JSON ----
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
[ -z "$INPUT" ] && exit 0
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
[ "$TOOL_NAME" = "Bash" ] || exit 0
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
[ -z "$CMD" ] && exit 0
# Single-line for matching against denylist patterns
CMD_FLAT=$(printf '%s' "$CMD" | tr '\n' ' ')
# ---- Denylist (skip antes de llamar fn match para ahorrar el invoke) ----
# Comandos demasiado cortos -> trivial
CMD_LEN=${#CMD_FLAT}
[ "$CMD_LEN" -lt 20 ] && exit 0
# Trivial single-utility commands
case "$CMD_FLAT" in
"ls"|"ls "*|"cd"|"cd "*|"pwd"|"pwd "*|"cat"|"cat "*|"echo"|"echo "*)
exit 0 ;;
"grep"|"grep "*|"head"|"head "*|"tail"|"tail "*|"wc"|"wc "*)
exit 0 ;;
"mkdir"|"mkdir "*|"rm"|"rm "*|"mv"|"mv "*|"cp"|"cp "*)
exit 0 ;;
"git"|"git "*)
exit 0 ;;
"go"|"go "*)
# go build / go test corrientes — el agente ya los maneja
exit 0 ;;
esac
# Comandos que ya usan el registry: ./fn ..., fn run ..., mcp__registry__*
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])\./fn([[:space:]]|$)'; then
exit 0
fi
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])fn[[:space:]]+(run|search|show|code|uses|doctor|index|match|list|add|proposal|sync|ops|check)'; then
exit 0
fi
# Pure-cd (movement only, no logic)
if printf '%s' "$CMD_FLAT" | grep -qE '^[[:space:]]*cd[[:space:]]+[^&|;]+$'; then
exit 0
fi
# ---- Llamar fn match con timeout 200ms ----
command -v timeout >/dev/null 2>&1 || exit 0
# Truncar el comando a algo razonable para fn match (evitar args huge)
CMD_TRUNC=$(printf '%s' "$CMD_FLAT" | head -c 500)
MATCH_JSON=$(timeout 0.2 "$FN_BIN" match "$CMD_TRUNC" --format json --top 3 2>/dev/null) || exit 0
[ -z "$MATCH_JSON" ] && exit 0
# ---- Parsear JSON ----
HIGH_CONF=$(printf '%s' "$MATCH_JSON" | jq -r '.high_confidence // false' 2>/dev/null || echo "false")
TOP_ID=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].id // ""' 2>/dev/null || echo "")
TOP_SCORE=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].score // 0' 2>/dev/null || echo "0")
TOP_SIG=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].signature // ""' 2>/dev/null || echo "")
TOP_SNIP=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].snippet // ""' 2>/dev/null || echo "")
[ -z "$TOP_ID" ] && exit 0
# Trigger condition: (high_confidence==true OR score>=0.85) AND score>=0.6
# - high_confidence requires top1/top2 gap > 1.5 (set por fn match)
# - score>=0.85 cubre matches muy fuertes donde el gap es modesto
SCORE_HI=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.85) ? "1" : "0" }')
SCORE_MIN=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.6) ? "1" : "0" }')
[ "$SCORE_MIN" = "1" ] || exit 0
if [ "$HIGH_CONF" != "true" ] && [ "$SCORE_HI" != "1" ]; then
exit 0
fi
# Truncar snippet a 100 chars y limpiar saltos de linea
SNIP_SHORT=$(printf '%s' "$TOP_SNIP" | tr '\n' ' ' | head -c 100)
# Formatear score con 2 decimales
SCORE_FMT=$(awk -v s="$TOP_SCORE" 'BEGIN{ printf "%.2f", s+0 }')
# ---- Emitir <system-reminder> a stderr ----
cat >&2 <<EOF
<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function.
USE: ./fn run $TOP_ID -> $TOP_SIG
SNIPPET: $SNIP_SHORT
Confidence: $SCORE_FMT. If you proceed inline, the violation will be logged.
</system-reminder>
EOF
exit 0
# Test manual:
# echo '{"tool_name":"Bash","tool_input":{"command":"taskkill.exe /IM registry_dashboard.exe /F"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
#
# Casos silenciosos:
# echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
# echo '{"tool_name":"Bash","tool_input":{"command":"./fn run filter_slice_go_core 1 2 3"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: recordatorio compacto de patrones canonicos del registry.
# Inyectado como additionalContext en cada turno del usuario.
# Issue 0085 (hardening 2).
#
# NUNCA bloquea. Solo printf de additionalContext.
set -euo pipefail
# Resolve registry root (walks up from cwd)
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
# Read input, extract session_id (UserPromptSubmit payload includes it)
INPUT=$(cat)
SESSION_ID=""
if command -v jq >/dev/null 2>&1; then
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || true)
fi
# Count current pending proposals + recent violations for situational awareness
PROPOSALS_PENDING="?"
VIOLATIONS_24H="?"
CALLS_24H="?"
CAP_CREATED=0
CAP_USED=0
CAP_ORPHAN=0
if command -v sqlite3 >/dev/null 2>&1; then
REG="$ROOT/registry.db"
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
[ -f "$REG" ] && PROPOSALS_PENDING=$(sqlite3 "$REG" "SELECT COUNT(*) FROM proposals WHERE status='pending'" 2>/dev/null || echo "?")
if [ -f "$MON" ]; then
VIOLATIONS_24H=$(sqlite3 "$MON" "SELECT COUNT(*) FROM violations WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)" 2>/dev/null || echo "?")
CALLS_24H=$(sqlite3 "$MON" "SELECT COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)" 2>/dev/null || echo "?")
if [ -n "$SESSION_ID" ]; then
sid_esc=$(printf '%s' "$SESSION_ID" | sed "s/'/''/g")
CAP_CREATED=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc'" 2>/dev/null || echo 0)
CAP_USED=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc' AND calls_in_session>0" 2>/dev/null || echo 0)
CAP_ORPHAN=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc' AND calls_in_session=0" 2>/dev/null || echo 0)
fi
fi
fi
REMINDER="REGISTRY-FIRST (issue 0085 telemetry active): "
REMINDER+="Inspect → mcp__registry__fn_search/show/code/uses/proposal. "
REMINDER+="Execute one fn → mcp__registry__fn_run or ./fn run. "
REMINDER+="Compose multi-fn → heredoc python IMPORTANDO del registry. "
REMINDER+="NUNCA sqlite3 registry.db directo (salvo schema/PRAGMA/COUNT/JOIN). "
REMINDER+="NUNCA reescribir inline logica que ya es funcion. "
REMINDER+="Si patron se repite >2x → propose nueva funcion via fn-constructor. "
REMINDER+="Estado: pending_proposals=${PROPOSALS_PENDING} violations_24h=${VIOLATIONS_24H} calls_24h=${CALLS_24H}. "
REMINDER+="CAPABILITY-GROWTH (issue 0086): created_this_session=${CAP_CREATED} used=${CAP_USED} orphan=${CAP_ORPHAN}. Si orphan>0 -> integra la funcion en el codigo o documenta por que se quedo huerfana. "
REMINDER+="Comando autocheck: /fn_claude."
jq -n --arg ctx "$REMINDER" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# PreToolUse hook: NO bloquea. Inyecta recordatorio cuando ve sqlite3 sobre registry.db
# para que el modelo prefiera el MCP `registry` la proxima vez.
input="$(cat)"
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // ""')"
# Solo nos importa registry.db (NO operations.db, NO otros .db).
if ! printf '%s' "$cmd" | grep -Eq 'sqlite3[^|]*\bregistry\.db\b'; then
exit 0
fi
# Casos legitimos donde el MCP no aplica: introspeccion de schema, agregaciones, JOINs.
if printf '%s' "$cmd" | grep -Eq '(\.schema|\.tables|PRAGMA[[:space:]]+(table_info|index_list))'; then
exit 0
fi
if printf '%s' "$cmd" | grep -Eqi '(COUNT\(|GROUP[[:space:]]+BY|JOIN[[:space:]])'; then
exit 0
fi
# Caso a redirigir: emitir nota como additionalContext y dejar pasar el comando.
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: "Aviso: sqlite3 directo sobre registry.db detectado. Para futuras consultas usa el MCP registry (mcp__registry__fn_search / fn_show / fn_code / fn_uses / fn_list_domains). Fallback a sqlite3 SOLO para .schema, PRAGMA, COUNT/GROUP BY, JOINs custom."
}
}'
exit 0
+1
View File
@@ -80,3 +80,4 @@ Thumbs.db
broken_paths.txt broken_paths.txt
imgui.ini imgui.ini
prompts/ prompts/
kotlin/functions/ui/
+6
View File
@@ -14,3 +14,9 @@
[submodule "cpp/vendor/implot3d"] [submodule "cpp/vendor/implot3d"]
path = cpp/vendor/implot3d path = cpp/vendor/implot3d
url = https://github.com/brenocq/implot3d.git url = https://github.com/brenocq/implot3d.git
[submodule "cpp/vendor/sdl3"]
path = cpp/vendor/sdl3
url = https://github.com/libsdl-org/SDL.git
[submodule "emsdk"]
path = emsdk
url = https://github.com/emscripten-core/emsdk.git
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"registry": {
"command": "./apps/registry_mcp/registry_mcp",
"args": ["--enable-run", "--enable-write"]
}
}
}
+224
View File
@@ -8,6 +8,230 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
## [Unreleased] ## [Unreleased]
## 2026-05-14
### Added
- **Issue 0086 — Monitor tab del `registry_dashboard`** (sub-repo `dataforge/registry_dashboard`). Pestaña `Monitor` primera y por defecto del TabBar, landing del bucle reactivo construir->ejecutar->recopilar->analizar->mejorar.
- 7 KPIs (Calls / MCP / Reg % / Errors / Violations / Copies / Versions) filtradas por ventana temporal (1h/24h/7d/30d/All).
- Sub-tab `Recent Executions` con columnas When/Function/Tool/ms/OK/Error. Columna Function muestra `$ <snippet>` en gris cuando `function_id` vacio, hover tooltip con comando completo. Checkbox `Only registry functions` filtra por `function_id != ''`.
- Sub-tab `Failed Functions` (5a) — subset filtrado a registry-functions fallidas, columnas When/Function/Tool/Error class/Error snippet, function_id en rojo.
- Live scatter `duracion (ms)` vs `time`: eje X auto-scroll a `now`, ventana configurable (1m/5m/15m/1h/6h) independiente del filtro de KPIs, eje Y dinamico `0..max(visible)+500ms`. Hora local (`UseLocalTime`). Series ok/error en verde/rojo. Hover sobre punto = tooltip Function/Tool/Duration/Error.
- Indicador `live`/`offline` con timestamp del ultimo evento WS.
- **WebSocket live stream sqlite_api -> registry_dashboard** (sub-repo `dataforge/sqlite_api`). Endpoint `GET /api/events/call_monitor`. Hub global con subscribers; ticker arranca solo con >=1 subscriber (cero overhead si nadie mira). Cliente recibe snapshot inicial (KPIs + 100 ultimas filas + watermark) y luego deltas `id > watermark`. Cliente puede mandar `{watermark: N}` para resumir tras reconexion.
- **WS client C++** hand-rolled RFC6455 en `ws_client.{h,cpp}` (~330 LOC) en el dashboard. Localhost-only (no TLS). Thread propio, reconnect exponencial 0.5s->8s, FIN/text/ping/pong/close handling, queue thread-safe drenada cada frame.
- **Migration 007 `command_snippet` en `calls`** (`projects/fn_monitoring/apps/call_monitor/migrations/007_calls_command_snippet.sql`). Aditiva, idempotente. Llena por hook `hook_call_monitor.sh` solo cuando `function_id == ''`. Redactado de `password=`/`token=`/`secret=`/`api_key=`/`bearer=`. Truncado 200 chars.
- **Issue 0087 — Capability Discovery Acceleration**. Modelo 5 capas + 7 piezas (ver `dev/issues/0087-*.md`).
- **`fn match`** (`cmd/fn/match.go`) — subcommand fuzzy-FTS5 que dado un comando devuelve top-N funciones del registry candidates. Latencia 6-7ms. Output JSON con `score` (normalizado top=1.0) + `raw_score` (absoluto pre-normalizacion) + `high_confidence` gate (`raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`).
- **`fn doctor capabilities --emit-claude-md`** (`cmd/fn/doctor.go` + `functions/infra/emit_capabilities_md.go`) — emite bloque markdown con secciones TOP 20 (por `calls_total`), Fresh 7d, Pipelines top 5. Fallback si `call_monitor.operations.db` ausente.
- **`call_monitor sequences --detect [--propose]`** (`projects/fn_monitoring/apps/call_monitor/sequences.go` + `migrations/006_function_sequences.sql`). Detecta secuencias A->B(->C) en `calls` (same session, gap < 30s, occ >= 5, sess >= 2, success_rate >= 0.9) y abre proposals `new_pipeline` automaticamente.
- **Hook `PreToolUse` `hook_fn_match.sh`** — denylist + `fn match` con timeout 0.2s. Inyecta `<system-reminder>FUZZY-MATCH: USE ./fn run <id>` cuando confidence alta. Latencia 113ms trigger / 32ms denylist. Registrado en `.claude/settings.local.json` (Bash matcher).
- **Hook `UserPromptSubmit` `hook_capabilities_inject.sh`** — cache 1h en `~/.cache/fn_registry/capabilities.txt`. Emite JSON `hookSpecificOutput.additionalContext` con linea compacta `CAPABILITIES: TOP / FRESH / PIPELINES`. Latencia cold 33ms / warm 18ms.
- **Timer systemd user** `call_monitor_sequences.timer` (OnCalendar 0/6h) + `.service` oneshot ejecutando `call_monitor sequences --detect --propose --report`. Versionado en `projects/fn_monitoring/apps/call_monitor/systemd/`.
- **3 funciones nuevas grupo `cpp-windows`** + pagina madre `docs/capabilities/cpp-windows.md`:
- `launch_cpp_app_windows_bash_infra``cmd.exe`/`PowerShell Start-Process` para lanzar exe en Windows desde WSL2.
- `is_cpp_app_running_windows_bash_infra``tasklist.exe /FI` con exit code 0/1 + stdout `RUNNING: PID=N MEM=K` o `NOT_RUNNING`.
- `redeploy_cpp_app_windows_bash_pipelines` — pipeline build? + deploy + launch + verify en 1 invocacion. Reemplaza ~6 commands manuales.
- **ADR 0004 `docs/adr/0004-telemetry-driven-capability-growth.md`** — formaliza el bucle telemetria -> proposal -> capability group -> discovery acceleration como motor de crecimiento del registry.
- **Regla `.claude/rules/function_growth_and_self_docs.md`** (entry #30 en `INDEX.md`) — contrato `.md` autosuficiente (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por promocion de composiciones, NO por inflado de funciones individuales.
### Changed
- **`.claude/CLAUDE.md` Norte ampliado** — 4o objetivo `PROMOVER COMPOSICIONES A PIPELINES` (el registry crece por composicion, no por inflado). Linea sobre auto-discovery zero-second-lookup.
- **`.claude/rules/registry_calls.md`** — clausula nueva: hooks e infraestructura de telemetria (`fn_match`, `fn doctor`, `call_monitor`) pueden leer `registry.db` directo con conexion read-only. NO sujeto a regla MCP-first (no son acciones del agente).
- **`/fn_claude` command** mejorado con objetivos del Monitor + interpretacion de `FUZZY-MATCH` hint + `CAPABILITIES` line + threshold semantica.
### Fixed
- **`launch_cpp_app_windows` quoting bug** — `cmd.exe /c "cd /d \"$dir\" && start ..."` rompia con paths Windows (el `\"` final se interpretaba como escape de comilla -> string sin cerrar -> "Windows cannot find \\"). Fix: reescribir a `powershell.exe -Command "Start-Process -FilePath ... -WorkingDirectory ..."` (single-quote PowerShell es literal, sin procesar `\` ni `$`).
- **`fn match high_confidence` siempre true** — debido a normalizacion `top=1.0`. Fix: añadir `raw_score` preservado pre-normalizacion + gate dual `raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`. Threshold 4.0 tuneado contra 14 patrones del analysis `domain_coverage_gaps` (~93% precision).
## 2026-05-07
### Added
- **`fn doctor` CLI** (`cmd/fn/doctor.go`) — entrypoint unico read-only para diagnostico del registry y artefactos. Subcomandos: `artefacts` (git/venv/app.md/upstream), `services` (apps tag service + systemctl + puerto), `sync` (drift `pc_locations` BD vs disco), `uses-functions` (imports reales vs declarados en `app.md`), `unused` (funciones sin consumidores). Flag `--json` para agentes/scripts. Cada subcomando es wrapper fino sobre una funcion del registry.
- `.claude/rules/fn_doctor.md` — regla 23 en `INDEX.md`. Documenta cuando usar, mapeo subcomando → funcion del registry, y acciones derivadas (que hacer cuando reporta un drift).
- `bash/functions/infra/backup_sqlite_db` (`backup_sqlite_db_bash_infra`, **impure**) — snapshot atomico de SQLite via `VACUUM INTO`. Mas seguro que `cp` con escrituras concurrentes.
- `bash/functions/infra/rotate_backups` (`rotate_backups_bash_infra`, **impure**) — retention rsnapshot-style `daily.N/weekly.M/monthly.K`.
- `bash/functions/infra/wait_for_http` (`wait_for_http_bash_infra`, **impure**) — poll URL hasta 2xx con timeout, util en deploys/smoke tests.
- `bash/functions/infra/wait_for_port` (`wait_for_port_bash_infra`, **impure**) — poll TCP host:puerto. Usa `nc` o `/dev/tcp` builtin (sin deps).
- `bash/functions/infra/port_kill` (`port_kill_bash_infra`, **impure**) — mata proceso(s) escuchando un puerto. Idempotente, fallback `KILL` tras `TERM`.
- `bash/functions/infra/tail_journal` (`tail_journal_bash_infra`, **impure**) — wrapper `journalctl` con auto-deteccion `--user` vs sistema, prioridad y `--since`.
- `bash/functions/infra/pre_commit_hook_install` (`pre_commit_hook_install_bash_infra`, **impure**) — instala hook que llama `scan_secrets_in_dirty_bash_cybersecurity` antes de cada commit. Idempotente con marca `fn_registry-pre-commit-v1`.
- `functions/infra/notify_telegram` (`notify_telegram_go_infra`, **impure**) — envia mensaje a chat Telegram via Bot API. Trunca >4096 chars.
- `functions/infra/artefact_doctor` (`artefact_doctor_go_infra`, **impure**) — audita salud de cada app/analysis: dir existe, `.git` presente, manifest parseable, `.venv` valido (analyses), upstream configurado.
- `functions/infra/services_status` (`services_status_go_infra`, **impure**) — apps con tag `service` + `systemctl is-active` (user/system) + puerto declarado en notes/description + check TCP localhost.
- `functions/infra/pc_locations_drift` (`pc_locations_drift_go_infra`, **impure**) — detecta drift `pc_locations` BD vs disco para el PC actual (`~/.fn_pc`). Tres tipos: `missing_on_disk`, `untracked_on_disk`, `status_should_be_active`.
- `functions/infra/audit_uses_functions` (`audit_uses_functions_go_infra`, **impure**) — para cada app Go/Py compara imports reales contra `uses_functions` del `app.md`. Reporta `missing_in_app_md` y `unused_in_app_md`. Heuristica documentada (puede dar falsos positivos en `unused`).
- `functions/infra/find_unused_functions` (`find_unused_functions_go_infra`, **impure**) — funciones del registry sin consumidores en otras funciones, apps o analyses. Pipelines sin tag `launcher` tambien aparecen.
- `bash/functions/pipelines/backup_all` (`backup_all_bash_pipelines`, **impure**, tag `launcher`) — orquesta `backup_sqlite_db` + `rotate_backups` sobre `registry.db`, cada `apps/*/operations.db`, y rsync `--link-dest` para vaults declarados en `projects/*/vaults/vault.yaml`.
### Changed
- `.claude/CLAUDE.md` — seccion CLI ampliada con comandos `fn doctor [subcommand] [--json]` y enlace a la regla.
- `.claude/rules/INDEX.md` — anadida fila 23 para `fn_doctor.md`.
### Fixed
- `functions/infra/pc_locations_drift.go``filepath.Join(absoluto, absoluto)` producia paths corruptos cuando `dir_path` ya era absoluto (caso comun: filas `pc_locations` traen path absoluto al disco del PC). Fix: chequear `filepath.IsAbs` antes de unir. Sintoma previo: todos los artefactos reportados como `missing_on_disk` aunque existieran.
- `go.mod``golang.org/x/net` movido a deps directas (`go mod tidy` tras anadir `notify_telegram`).
### Notes
- Hallazgo de la primera ejecucion `fn doctor uses-functions`: 7/12 apps con drift real (`auto_metabase`, `dag_engine`, `deploy_server`, `docker_tui`, `kanban`, `metabase_registry`, `script_navegador`). Pendiente sincronizar sus `app.md` con los imports reales en sesion futura.
- `fn doctor unused` muestra muchas funciones core sin consumidores aun (`compose2_go_core`, `curry2_go_core`, etc.). Esperado: el registry crece antes que las apps que las consuman.
## 2026-05-04
### Added
- `cpp/functions/viz/graph_labels_select` (`graph_labels_select_cpp_viz`, **pure**) — TU separado de `graph_labels` con los helpers puros `graph_compute_degrees` y `graph_labels_select` (frustum cull + always_for_* + top-N por `size * (degree+1)`). Vive en su propio archivo para que los tests unitarios lo cubran sin abrir ImGui.
- `cpp/functions/viz/graph_viewport_selection` (`graph_viewport_selection_cpp_viz`, **pure**) — TU separado de `graph_viewport` con `clear_selection`, `is_selected`, `add_to_selection`, `toggle_selection`. Mantienen sincronizados `state.selection` y `nodes[i].flags & NF_SELECTED`.
- `cpp/functions/viz/graph_types` (`graph_types_cpp_viz`, **pure**) — TU de implementacion de `GraphData::update_bounds()` y `GraphData::find_node_by_user_data()`. Pareja obligatoria del header del tipo (`graph_types.h` indexado en `types/viz/`).
- `cpp/apps/chart_demo/app.md` — la demo de primitivos viz (line/scatter/bar/heatmap) ahora aparece en el registry como `chart_demo_cpp_viz`.
- `cpp/apps/shaders_lab/app.md` — el live GLSL playground con DAG ahora tiene `app.md` propio (antes solo existia entrada legacy en BD sin `.md` en disco).
### Changed
- `registry/indexer.go` — el indexer ahora escanea tambien `<lang>/apps/*/app.md` (mismo patron que ya usaba para `<lang>/functions/` y `<lang>/types/`). Antes solo veia `apps/` y `projects/*/apps/` — las apps en `cpp/apps/` quedaban invisibles. `./fn index` reporta 17 apps (antes 15).
- `cpp/functions/viz/graph_labels.md``signature` reducida a `graph_labels_draw` y `graph_labels_draw_at` (los helpers puros pasan a entrada propia). `uses_functions` apunta a la nueva entrada `graph_labels_select_cpp_viz`.
- `cpp/functions/viz/graph_viewport.md``uses_functions` añade `graph_viewport_selection_cpp_viz`.
- `projects/osint_graph/apps/graph_explorer/app.md``uses_functions` sincronizado con `CMakeLists.txt`: ahora declara las 23 funciones del registry que enlaza (antes 15). Añadidas: `graph_viewport_selection`, `graph_labels_select`, `graph_types`, `graph_spatial_hash`, `button`, `icon_button`, `badge`, `empty_state`.
- `projects/fn_monitoring/apps/registry_dashboard/app.md``uses_functions` sincronizado con `CMakeLists.txt` (21 deps, antes 9). Añadidas: `badge`, `button`, `empty_state`, `icon_button`, `modal_dialog`, `page_header`, `process_runner`, `process_state_machine`, `select`, `text_input`, `toast`, `toolbar`, `tree_view`. Removido: `fps_overlay` (vive en `fn_framework`, no se declara).
### Decisions
- ADR `0003-orphan-tu-as-separate-function-entry.md` — cuando una funcion del registry necesita partir 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. El parent declara la nueva entrada en `uses_functions`. Razon: el indexer asume `1 .cpp = 1 .md`; un `file_path` multi-archivo rompe la convencion y deja apps nuevas sin saber que TUs enlazar.
### Added — sesion NER+RE para graph_explorer (tarde, 980 → 990 funciones)
**18 funciones nuevas** sobre el ecosistema NER+RE, en dos rondas de `fn-constructor`:
Ronda 1 — extraccion de relaciones (mREBEL/REBEL/MarianMT):
- `python/functions/datascience/parse_rebel_output.py` (pure) — parser wire `<triplet>` REBEL/mREBEL.
- `python/functions/datascience/align_relations_to_entities.py` (pure) — string-match aligner.
- `python/functions/datascience/mrebel_load_model.py` (impure, **CC BY-NC-SA 4.0 — NO comercial**).
- `python/functions/datascience/mrebel_base_load_model.py` (impure, misma licencia).
- `python/functions/datascience/rebel_load_model.py` (impure, **Apache 2.0**, EN-only).
- `python/functions/datascience/marianmt_es_en_load_model.py` (impure) — Helsinki-NLP/opus-mt-es-en.
- `python/functions/datascience/translate_es_to_en.py` (impure) — wrapper traduccion frase a frase.
- `python/functions/datascience/extract_relations_mrebel.py` (impure) — pipeline mREBEL frase-a-frase + alineamiento.
- 21 tests pytest verdes.
Ronda 2 — pipeline GLiNER2 + OpenIE schema-less + composicion (tarde):
- `python/functions/core/clean_pdf_text.py` (pure) — limpia artefactos PyPDF2.
- `python/functions/core/chunk_with_overlap.py` (pure) — sliding window con avance forzado.
- `python/functions/core/merge_entity_aliases.py` (pure) — coreferencia normalize+substring.
- `python/functions/core/filter_relations_by_entity_types.py` (pure) — post-filter typed.
- `python/functions/core/aggregate_extraction_results.py` (pure) — dedupe + Counter sobre N chunks.
- `python/functions/datascience/gliner2_load_model.py` (impure, **Apache 2.0**) — `fastino/gliner2-large-v1`.
- `python/functions/datascience/extract_graph_gliner2.py` (impure) — wrapper schema + threshold + include_confidence.
- `python/functions/datascience/spacy_es_load_model.py` (impure) — `es_core_news_md` cacheado.
- `python/functions/datascience/extract_triples_spacy_es.py` (impure) — OpenIE schema-less ES por reglas de dependencia (verbo del texto = predicado).
- `python/functions/pipelines/extract_graph_from_text.py` (impure pipeline) — composicion E2E: chunk → extract_graph_gliner2 (×N) → aggregate → filter typed → merge aliases → grafo final.
- 39 tests pytest verdes.
### Added — analysis `gliner_glirel_tuning`
`projects/osint_graph/analysis/gliner_glirel_tuning/` — investigacion empirica de modelos NER/RE. **9 notebooks** ejecutados:
| # | Notebook | Hallazgo clave |
|---|---|---|
| 01 | `01_gliner_glirel_tuning.ipynb` | Calibracion de thresholds GLiNER+GLiREL |
| 02 | `02_e2e_spanish_graph.ipynb` | E2E texto ES — descubrimiento del fail de GLiREL en castellano |
| 03 | `03_mrebel_vs_glirel.ipynb` | mREBEL gana a GLiREL pero CC BY-NC-SA |
| 04 | `04_gliner2_winner.ipynb` ⭐ | **GLiNER2 (Apache 2.0, NER+RE joint, 340M)** elegido como motor principal |
| 05 | `05_long_text_and_pdf.ipynb` | Pipeline PDF E2E sobre `politica_proteccion_datos.pdf` (BBVA, 89.882 chars) |
| 06 | `06_improvements.ipynb` | Threshold 0.3 (vs default 0.5) → +187% relaciones; coref reduce 18% aislados |
| 07 | `07_nuextract_vs_gliner2.ipynb` | NuExtract GPU 2.6× mas lento, calidad similar — descartado por defecto |
| 08 | `08_improving_gliner2.ipynb` | snake_case verbal labels + post-filter typed = mejor combo |
| 09 | `09_spacy_es_openie.ipynb` | spaCy ES dep-rules: schema-less, predicado = verbo del texto |
### Added — vault `osint_nlp_models`
`projects/osint_graph/vaults/osint_nlp_models` (symlink a `~/vaults/osint_nlp_models/`):
- `models/` — fichas de gliner, glirel, mrebel, gliner2, candidates a probar.
- `decisions/` — 3 ADRs cortos del 2026-05-04 (mrebel-over-glirel mañana, gliner2-over-mrebel tarde, license-constraint).
- `benchmarks/corpus_v1.md` + `results_log.csv` (15 filas de experimentos).
- `test_documents/politica_proteccion_datos.pdf` (PDF de BBVA copiado para reproducibilidad).
### Added — playground HTML
`projects/osint_graph/analysis/gliner_glirel_tuning/playground/`:
- `server.py` — FastAPI con GLiNER2 cacheado, endpoints `GET /` (HTML) y `POST /extract` (texto → grafo).
- `index.html` — UI: textarea, KPIs (nodos/aristas/tiempo), grafo Sigma.js, JSON exportable.
- `static/sigma.min.js` + `graphology.umd.min.js` (servidos localmente para evitar bloqueo CDN por extensiones tipo MetaMask/SES).
Stack aplicado por el server:
1. snake_case verbal labels (`works_at`, `ceo_of`, `headquartered_in`, `agreement_with`...)
2. threshold 0.3 (configurable)
3. chunking automatico > 1500 chars
4. post-filter typed (`(person, organization)` validos por relacion)
5. coreferencia normalize+substring
6. layout server-side via `networkx.spring_layout`
7. render Sigma.js (sin fisica → sin loops de ResizeObserver)
### Added — issues
- `dev/issues/0050-jupyter-exec-collab-client-failure.md` — bug `jupyter_exec` con cliente colaborativo + workaround documentado.
- `projects/osint_graph/apps/graph_explorer/issues/0041-split-confidence-thresholds.md` — split `confidence_threshold` en `entity_threshold` + `relation_threshold`.
- `projects/osint_graph/apps/graph_explorer/issues/0042-gliner2-unified-extractor.md` ⭐ — sustituir GLiREL por GLiNER2 en `extract_graph_hybrid`. Reemplaza 0042-mrebel.
- `projects/osint_graph/apps/graph_explorer/issues/0042-mrebel-relation-extractor.md.superseded` — version mREBEL del 0042 archivada al ganar GLiNER2.
### Changed
- `cpp/CMakeLists.txt``_GE_DIR` y `_DASH_DIR` sobreescribibles via `-D<...>=<path>` para builds en worktrees (commit `e72d6364`). Habilita `parallel-fix-issues` sobre apps C++.
- `python/functions/datascience/glirel_load_model.py` — workaround compat `huggingface_hub` 1.x: classmethod monkey-patch idempotente para inyectar `proxies`/`resume_download` que el HF nuevo dejo de pasar (commit `3b3378cf`).
- Sub-repo `dataforge/graph_explorer` master local: merges `--no-ff` de `issue/0035e-polish-and-tests` (commit `f614a51`) + `issue/0013-paste-extract-panel` (commit `2a49c2b`). 125/125 tests pytest verdes. **Sin push aun** — pendiente confirmacion + validacion Windows.
### Fixed (bugs encontrados + raiz + fix)
| Bug | Raiz | Fix |
|---|---|---|
| `chunk_with_overlap` bucle infinito | Frase mas larga que `max_chars`, no avanzaba `i`, OOM-killed por overlap acumulado | Avance forzado: meter al menos UNA frase aunque exceda `max_chars` |
| NuExtract degenera en texto largo | Sin `repetition_penalty`, decoder entra en bucle de tokens repetidos hasta agotar 2048 max_new_tokens | `repetition_penalty=1.15` + chunking obligatorio (179/179 chunks parsed OK tras fix) |
| NuExtract `AutoProcessor.from_pretrained` rota en transformers 5.x | Sub-processor de video tira `TypeError: argument of type 'NoneType' is not iterable` (Qwen2-VL) | Bypass: `AutoTokenizer` + `AutoModelForImageTextToText` directamente |
| Vis-network ResizeObserver loop spam (en SES/MetaMask) | Vis-network usa physics simulation → ResizeObserver dispara warnings amplificados por SES | Migrar a Sigma.js + layout server-side via `networkx.spring_layout` (sin fisica frontend) |
| `jupyter_exec append` HTTP 405 | `jupyter_nbmodel_client` espera collab WebSocket Y.js, no soportado al 100% por jupyter-collaboration nuevo | Documentado en issue 0050; workaround actual: build_notebook scripts con `nbformat` + `nbconvert --execute` |
| Kernel startup shadows pip packages | `00_fn_registry.py` añade cada subdir de `python/functions/` a sys.path top-level → `bigquery/datasets.py` shadows HF `datasets` package needed by transformers | Workaround per-notebook: `sys.path = [p for p in sys.path if not p.startswith(_pf+'/')]` + añadir solo el padre. Issue futuro pendiente. |
### Decisions — vault ADRs
| Decision | Razon |
|---|---|
| **GLiNER2 (Apache 2.0)** sustituye a GLiREL en `extract_graph_hybrid` | 6/8 relaciones correctas vs 0/1 de GLiREL en es_corporate_short, 1.18s vs 22s de mREBEL, NER+RE en una pasada |
| mREBEL queda como fallback (no comercial) | 4/5 correctas pero CC BY-NC-SA 4.0 + 25× mas lento |
| spaCy ES dep-rules para OpenIE schema-less | Predicado = verbo del texto (`querer`, `abrazar`), 5ms/frase, sin alucinaciones |
| Threshold `0.3` (vs default `0.5`) sweet spot | +187% relaciones manteniendo precision; 0.2 mete +22% entidades dudosas |
| Coreferencia normalize+substring + post-filter typed = **gratis y decisivos** | Coref 18% aislados; post-filter elimina `Madrid president_of Persona` |
| Translate ES→EN + triplet-extract EN **NO** vale la pena | Pierdes verbos del texto (`querer``loves`), +500ms-1s, +300MB MarianMT, riesgo nombres propios |
## 2026-04-28
### Added
- `cpp/functions/core/app_about` (`app_about_cpp_core`) — ventana flotante About con `about_window_set_info(project, version, description)`, `about_window_menu_item("About...")` y `about_window_render()`. Render automatico via `fn::run_app` (cableado en `cpp/framework/app_base.cpp`).
- `bash/functions/infra/ensure_repo_synced` (`ensure_repo_synced_bash_infra`) — pipeline idempotente que compone `gitea_create_repo` + `gitea_push_directory`: crea repo Gitea si falta, inicializa `.git` local si falta, commitea cambios pendientes y pushea. Defaults: owner `dataforge`, branch `master`.
- `analysis.md` para 6 analyses que estaban en disco pero sin indexar: `agent_coding_eval`, `estudio_embeddings`, `estudio_mercados`, `ontology_graph`, `pruebas_jupyter`, `retrieving_graphs`. Ahora `./fn index` reporta 8 analyses (antes 2).
- Repos `dataforge/<name>` creados en Gitea para apps y analyses que no estaban subidos: `agents_and_robots`, `element_matrix_chat`, `deploy_server`, `shaders_lab`, `voice_guide`, `agent_coding_eval`, `ontology_graph`, `turismo_spain`. Cada uno con `.gitignore` apropiado para excluir binarios, `.venv/`, `node_modules/`, `.jupyter*`, `operations.db*`.
### Changed
- `cpp/functions/core/app_menubar`: el item top-level `Settings...` pasa a ser un `BeginMenu("Settings")` con dos subitems: `Settings...` (ventana de `app_settings`) y `About...` (nuevo, ventana de `app_about`). Las apps que usan `fn_ui::app_menubar(nullptr, 0, nullptr)` heredan el cambio sin tocar nada.
- `projects/fn_monitoring/apps/registry_dashboard/main.cpp`: cablea `fn_ui::about_window_set_info("fn_registry Dashboard", "0.2.0", "...")` antes de `fn::run_app`. Tabla `Apps` gana columna `Git` con valores `remote` (repo_url poblado), `local` (.git/ presente) o `-`.
- `data.h`/`data.cpp`/`data_http.cpp` del dashboard: `AppRow` extendido con `repo_url` y `dir_path`.
- 10 repos migrados de branch `main` a `master` para unificar convencion: `apps/{docker_tui,fuzzygraph,metabase_registry,pipeline_launcher,rapid_dashboards,script_navegador}`, `analysis/{estudio_embeddings,estudio_mercados,pruebas_jupyter,retrieving_graphs}`. Default branch en Gitea actualizado via API (`PATCH /repos/{owner}/{repo}` con `{"default_branch":"master"}`), branch `main` remota borrada.
- `git config --global init.defaultBranch master` para que los proximos `git init` sean consistentes.
- `/full-git-push`: descubre apps/analyses sin `.git` y ofrece inicializarlos con `ensure_repo_synced` automaticamente. Excluye `subrepos/` para evitar duplicacion (mirrors upstream).
- `/full-git-pull`: tras `fn sync`, segunda pasada que clona los `dataforge/<name>` registrados en `apps`/`analysis` que no existan localmente — soluciona el "no pude recuperar la app en el otro PC".
- `bash/functions/infra/ensure_repo_synced.sh`: localiza dependencias via `FN_REGISTRY_INFRA_DIR` o `FN_REGISTRY_ROOT`, robusto a sourcing desde zsh/bash.
### Fixed
- `projects/fn_monitoring/apps/sqlite_api/handlers.go|main.go|handlers_test.go` + nuevos `handlers_mutations.go` y `handlers_projects.go`: cableados endpoints `POST /add_app|add_analysis|add_vault|reindex` y `GET /projects` para que el dashboard pueda crear artefactos y navegar projects desde la actions bar (estado pendiente de varios dias en uncommitted, ahora versionado en `dataforge/sqlite_api`).
- Bug operativo en `sqlite_api` (Windows): `SO_RCVTIMEO` se pasaba como `struct timeval` cuando Windows espera `DWORD ms` → timeout efectivo de 5 ms. Ya documentado en `app.md` del dashboard.
## 2026-04-24 ## 2026-04-24
### Added ### Added
+1 -2
View File
@@ -12,10 +12,9 @@ uses_functions:
- parse_cron_expr_go_core - parse_cron_expr_go_core
- next_cron_time_go_core - next_cron_time_go_core
- cron_ticker_go_infra - cron_ticker_go_infra
- cron_match_go_core - find_go_core
- process_spawn_go_infra - process_spawn_go_infra
- process_wait_go_infra - process_wait_go_infra
- process_kill_go_infra
uses_types: uses_types:
- dag_definition_go_core - dag_definition_go_core
- dag_step_go_core - dag_step_go_core
+65 -311
View File
@@ -2,9 +2,10 @@
name: shaders_lab name: shaders_lab
lang: cpp lang: cpp
domain: gfx domain: gfx
description: "Live GLSL fragment shader playground. Editor de codigo + node-editor visual (DAG con multi-source) + Functions palette drag-and-drop, dos canvas paralelos (Code y DAG), thumbnails per-nodo, todo nativo C++17 + ImGui + OpenGL 3.3 + imgui-node-editor." 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: [gui, shaders, opengl, glsl, imgui, node-editor, dag, vj] tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
uses_functions: uses_functions:
# gfx
- gl_loader_cpp_gfx - gl_loader_cpp_gfx
- gl_shader_cpp_gfx - gl_shader_cpp_gfx
- gl_framebuffer_cpp_gfx - gl_framebuffer_cpp_gfx
@@ -15,336 +16,89 @@ uses_functions:
- dag_catalog_cpp_gfx - dag_catalog_cpp_gfx
- dag_compile_cpp_gfx - dag_compile_cpp_gfx
- dag_uniforms_cpp_gfx - dag_uniforms_cpp_gfx
- dag_palette_cpp_gfx - dag_panel_cpp_gfx
- dag_node_editor_cpp_gfx - dag_node_editor_cpp_gfx
- dag_palette_cpp_gfx
- dag_node_previews_cpp_gfx - dag_node_previews_cpp_gfx
- shaderlab_db_cpp_gfx - shaderlab_db_cpp_gfx
- code_to_generator_cpp_gfx - code_to_generator_cpp_gfx
- fps_overlay_cpp_core # core (modal Save-as-generator)
- panel_menu_cpp_core - modal_dialog_cpp_core
- layouts_menu_cpp_core - text_input_cpp_core
- app_menubar_cpp_core - button_cpp_core
- layout_storage_sqlite_cpp_core uses_types:
uses_types: [] - dag_types_cpp_gfx
framework: "imgui + opengl3 + imgui-node-editor" framework: "imgui"
entry_point: "cpp/build/linux/apps/shaders_lab/shaders_lab" entry_point: "main.cpp"
dir_path: "cpp/apps/shaders_lab" dir_path: "cpp/apps/shaders_lab"
repo_url: ""
--- ---
## Descripcion ## Arquitectura
App de live-coding y composicion de fragment shaders GLSL con dos modos coexistentes: editor de codigo libre y editor de DAG visual con catalogo de nodos arrastrables. Pensada para jugar con shaders de tipo VJ y para extraer funciones GLSL al registry global. App ImGui de live-coding GLSL con dos modos en paralelo:
## Estado actual 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**.
### Fase 1 — Core renderer + editor de codigo `[done]` Al guardar un Code shader como "generator" se traduce a un `DagNodeDef` y se
- Ventana ImGui + OpenGL 3.3 via `fn::run_app` del framework. persiste en `shaders_lab.db` (tabla via `shaderlab_db`), apareciendo en la
- Editor de texto del fragment shader con recompile auto (debounce 250 ms). paleta del DAG junto a los builtins.
- Render a FBO + `ImGui::Image` para mostrar preview en panel propio.
- Errores de compilacion con linea, footer rojo en el panel Code.
- 3 presets seed: Plasma, Circle, Checker.
- Cross-compile a Windows con loader propio (`gl_loader`) — sin dependencias externas mas alla de GLFW/ImGui ya vendorizados.
### Fase 2 — Annotated uniforms + auto-controls `[done]` ## Capas
- Parser de comentarios `// @slider`, `// @color`, `// @toggle`, `// @xy` sobre las declaraciones de uniforms.
- Panel `Controls` con widgets ImGui auto-generados (sliders, color pickers, checkboxes).
- Sincronizacion de valores entre recompilaciones por nombre del uniform.
- Tests inline para `uniform_parser` (6/6 asserts).
### Fase 3 — DAG mode `[done]` | Archivo | Responsabilidad |
- Catalogo de 11 nodos: 4 Gen (`solid`, `gradient`, `plasma`, `circle`), 3 Op (`invert`, `gamma`, `hueShift`), 3 Blend (`mix`, `multiply`, `screen`), 1 Output. |---|---|
- Compilador `compile_dag_to_glsl(pipeline)` que emite un fragment shader unico con `vec4 node_<i>(...)` por nodo y main encadenando outputs. | `main.cpp` | UI shell, paneles, modal save-as, layouts, AppConfig |
- Multi-source: hasta 4 inputs por nodo via `source_ids[4]`. Compilador resuelve cada slot. | `compiler.cpp` | `compile_code()`, `compile_dag()`, `mark_code_dirty()` con debounce 250ms |
- Nodo `Output` (sink, rojo, no borrable): su `source_ids[0]` decide que va a `fragColor`.
- Tests inline para `dag_compile` (6/6) y `dag_catalog` (8/8).
### Fase 4a — Layout multi-ventana + dos canvas `[done]` `main.cpp` mantiene estado global de sesion (g_source, g_pipeline, g_descs,
- Cada panel es ventana ImGui dockable independiente: `Code`, `DAG Pipeline`, `Canvas Code`, `Canvas DAG`, `Controls`, `Generated GLSL`, `Functions`. g_store, g_layouts...) — ImGui retained-mode obliga a que persista entre
- Dos `ShaderCanvas` simultaneos: el del Code y el del DAG renderizan en paralelo, cada uno con su FBO y programa propio. frames. Toda la logica pura de compilacion vive en `compiler.cpp` y en las
- Sin focus-based recompile: cada fuente recompila solo cuando su contenido cambia. funciones `dag_compile`, `code_to_generator`, `uniform_parser` del registry.
### Fase 4b — Visual node editor (imgui-node-editor) `[done]` ## Persistencia
- Vendorizada `imgui-node-editor` de thedmd en `cpp/vendor/imgui-node-editor/` (parche puntual en `imgui_extra_math.inl` para evitar choque con ImGui 1.92.7).
- Layout 3 columnas por nodo: pines input a la izquierda, controles en el centro, pin output a la derecha.
- Pines como circulos de radio 9 pegados al borde del nodo (mitad fuera, mitad dentro), color uniforme neutro (data type uniforme = `vec4`).
- `ed::PinRect` cubre el circulo entero — la mitad sobresaliente sigue siendo grabbable.
- Cables 2.5px del color del pin.
- Node drag, pan, zoom — todo nativo del editor.
- Topology change disparado solo cuando se anaden/quitan/reconectan nodos. Mover sliders no recompila.
### Fase 4c — Functions palette drag-drop `[done]` - **`shaders_lab.db`** (junto al .exe) — tabla de generators de usuario via
- Ventana `Functions` con catalogo agrupado en `Generators / Operators / Blends`. `shaderlab_db_*`, ademas de `imgui_layouts` (creada por `layout_storage`).
- Cada item es drag source con payload `DAG_NODE_TYPE`. - `imgui.ini` y `app_settings.ini` — gestionados por `fn::run_app` en
- Drop sobre el canvas del DAG anade el nodo en la posicion del mouse. `<exe_dir>/local_files/`.
- Sin botones `+ Add Node` / `Clear` — todo flujo via drag-drop.
- Output node nunca aparece en la paleta (sink unico fijo).
### Fase 4d — UX deletes + cycle check real `[done]` ## Paneles
- **Right-click sobre cable**: borra ese link.
- **Right-click sobre pin output**: limpia el fan-out completo (todos los inputs que apuntaban a este nodo).
- **Right-click sobre pin input**: limpia ese slot.
- **Doble right-click sobre nodo**: borra el nodo (Output protegido).
- Validacion de ciclo via DFS sobre `source_ids` (no por indice del vector); `topo_sort` reordena el pipeline tras cada cambio para mantener `out_<i>` coherentes.
- Drop de nuevo nodo se inserta antes del Output, no al final.
### Fase 4e — Per-node preview `[done]` | Panel | Atajo | Que muestra |
- Toggle `[+] preview` / `[-] preview` en cada nodo no-Output (off por defecto). |---|---|---|
- Cada nodo abierto tiene su FBO de 96x64 keyed por `editor_uid`. | Code | Ctrl+1 | Editor del fragment shader + boton "Save as generator" |
- Compilador emite `uniform int u_preview_target` y branches `if (u_preview_target == i) { fragColor = out_i; return; }`. | DAG Pipeline | Ctrl+2 | Node editor con la pipeline |
- `dag_previews_render` itera nodos con preview abierto, dibuja al FBO con ese index. | Canvas Code | Ctrl+3 | Render del Code shader |
- Sin recompile al togglear preview ni al mover sliders — un solo programa GL. | Canvas DAG | Ctrl+4 | Render del shader compilado del DAG |
| Controls | Ctrl+5 | Sliders/color pickers de uniforms anotados |
### Fase 5 — SQLite + custom generators desde el Code `[done]` | Functions | Ctrl+6 | Paleta del DAG (generators + filters + output) |
- **`u_params` a tamaño dinámico**: array global `vec4 u_params[64]` (256 floats), cada nodo ocupa `ceil(param_count/4)` vec4s consecutivos. `dag_param_layout(pipeline)` calcula el indice base por nodo; compilador y `dag_uniforms_apply` lo comparten. `DagStep::params` y `DagNodeDef::param_*` pasan a `vector<>`. | Generated GLSL | Ctrl+7 | GLSL final del DAG con uniforms baked como const array |
- **Nuevos Gen nodes (8)**: `checker`, `stripes`, `dots`, `rings`, `polar_rays`, `noise_value`, `voronoi`, `truchet`. Catalogo total: 19 nodos (4 originales + 8 nuevos Gen + 4 Op + 3 Blend + Output).
- **Bug fix `solid`**: el control Color con `ImGuiColorEditFlags_NoLabel` no mostraba el nombre. Ahora `dag_node_editor` imprime `TextUnformatted(label) + SameLine` antes del swatch.
- **Persistencia `shaders_lab.db`** (SQLite local en `apps/shaders_lab/shaders_lab.db`): tabla `generators` con `id, label, description, source_glsl, body_glsl, param_count, param_defaults, param_names, controls, tags, timestamps`. Funcion `shaderlab_db` (CRUD) testeada (7/7) y reutilizable.
- **Catalogo mutable**: `dag_register_node()` / `dag_unregister_node()`. Built-ins protegidos via flag `is_builtin`.
- **Code → Generator**: funcion pura `code_to_generator(source)` traduce el GLSL del Code en un body de Gen + DagControl[] (testeada 7/7). Cada uniform anotado se convierte en su control (slider/xy/color); cada uniform reclama 1 vec4 entero. El body se transforma asi: lineas `vec2 uv = ...` eliminadas, `fragColor = X;` -> `return X;`, locales `<type> <name> = u_params[__BASE__+i].swizzle;` prependidas. La lambda `body_glsl` substituye `__BASE__` con el indice runtime.
- **UI**: boton `Save as generator...` en el panel `Code` con modal (name snake_case + label + description + tags). Tras guardar, el nodo aparece en la paleta `Functions`. Al arrancar, `load_user_generators_into_catalog()` re-traduce y registra los persistidos.
- **Quitados**: botones de presets `Plasma / Circle / Checker` y el archivo `seed_shaders.h`. Default del Code = un placeholder con uniforms anotados como ejemplo.
### Fase 6 — Menubar reusable (View + Layouts) `[done]`
App estrena una `BeginMainMenuBar` con dos menus, cableada via `app_menubar_cpp_core`:
- **View** (`panel_menu_cpp_core`): MenuItem checkable por cada uno de los 7 paneles (`Code`, `DAG Pipeline`, `Canvas Code`, `Canvas DAG`, `Controls`, `Functions`, `Generated GLSL`). Cada bool `g_show_*` se comparte con el `bool*` de `ImGui::Begin(name, &g_show_X)`, asi que la X de cada ventana sincroniza con el menu. Cada `Begin/End` envuelto en guard para no llamar `End` si el panel esta oculto.
- **Layouts** (`layouts_menu_cpp_core`): captura del layout actual de ImGui (`SaveIniSettingsToMemory`) bajo un nombre, persistido en la tabla `ui_layouts(name, blob, created_at, updated_at)` de `shaders_lab.db`. Items:
- Lista de layouts guardados (click → apply, marker `* ` en el activo).
- `Save current as...` (popup con InputText).
- `Delete` (submenu listando los layouts).
- `Reset to default` (abre todos los paneles, limpia marker activo).
Detalles tecnicos:
- `LoadIniSettingsFromMemory` se difiere al inicio del frame siguiente via `g_pending_layout_blob` (no se puede llamar mid-frame entre `NewFrame` y `Render`).
- `shaders_lab.db` se reutiliza para `ui_layouts` via nuevo getter `shaderlab_db_handle()` — una sola conexion SQLite para generators y layouts.
- Las callbacks (`list/on_apply/on_save/on_delete/on_reset`) se cablean en `main()` con lambdas que envuelven las primitivas CRUD de `layout_storage_sqlite_cpp_core`.
### Como usarlo en otras apps
Patron reusable de tres pasos:
```cpp
#include "core/app_menubar.h"
#include "core/layout_storage_sqlite.h"
// 1. Declarar bools de visibilidad por panel
static bool g_show_foo = true;
static bool g_show_bar = true;
// 2. Declarar callbacks y blob diferido
static fn_ui::LayoutCallbacks g_layout_cb;
static std::string g_pending_blob;
static std::string g_pending_name;
// 3. En main(), cablear callbacks contra tu sqlite3*
fn_ui::layout_storage_init(db);
g_layout_cb.list = [db]{ return fn_ui::layout_storage_list(db); };
g_layout_cb.on_apply = [db](const std::string& n) {
g_pending_blob = fn_ui::layout_storage_load_blob(db, n);
g_pending_name = n;
};
g_layout_cb.on_save = [db](const std::string& n) {
size_t sz = 0;
const char* b = ImGui::SaveIniSettingsToMemory(&sz);
if (b && sz) fn_ui::layout_storage_save(db, n, std::string(b, sz));
g_layout_cb.active_name = n;
};
g_layout_cb.on_delete = [db](const std::string& n) {
fn_ui::layout_storage_delete(db, n);
if (g_layout_cb.active_name == n) g_layout_cb.active_name.clear();
};
g_layout_cb.on_reset = []{ /* abrir todos los paneles, limpiar active_name */ };
// 4. En render(), aplicar pendientes y llamar app_menubar
void render() {
if (!g_pending_blob.empty()) {
ImGui::LoadIniSettingsFromMemory(g_pending_blob.c_str(), g_pending_blob.size());
g_layout_cb.active_name = g_pending_name;
g_pending_blob.clear(); g_pending_name.clear();
}
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
fn_ui::PanelToggle toggles[] = {
{"Foo", "Ctrl+1", &g_show_foo},
{"Bar", "Ctrl+2", &g_show_bar},
};
fn_ui::app_menubar(toggles, std::size(toggles), &g_layout_cb);
if (g_show_foo) {
if (ImGui::Begin("Foo", &g_show_foo)) { /* ... */ }
ImGui::End();
}
// ...
}
```
### Fase 7 — UX node editor + DAG correctness `[done]` (2026-04-25)
Pulido de la edición visual del DAG y corrección de fugas en el render. Sin cambio de schema ni de catalog público (más allá del `dag_register_node` ya añadido en Fase 5).
- **Nodos más grandes para conectar más rápido** (`dag_node_editor.cpp`):
- `PIN_RADIUS` 9 → **14 px** (área de grab ~2.5×). `PIN_DIAMETER`, `CABLE_THICK` 2.5 → **3.5**, borde de pin 1.5 → 2.0.
- `CONTROL_WIDTH` constante 150 → **220 px**, `COL_GAP` 8 → **14 px**, `NodePadding` vertical 8 → 12.
- Espaciado inicial entre nodos auto-colocados 220 → 320 px.
- **Bug fix `solid` sin label**: el control `Color` usaba `ImGuiColorEditFlags_NoLabel`, así que el swatch era el único contenido del nodo y parecía "sin nombre ni parámetro". Fix en `dag_node_editor.cpp`: imprimir `ImGui::TextUnformatted(ctrl.label) + SameLine` antes del swatch. Aplica a todo control de tipo `Color`, no solo a `solid`.
- **Strict output** (`dag_compile.cpp`): eliminado el fallback `last_valid_out` que filtraba el output del último nodo evaluado cuando `Output` no tenía source o no existía. Ahora la regla es: solo se emite lo conectado al nodo `Output`; en cualquier otro caso `seed()` (gris oscuro `vec4(0.04, 0.04, 0.06, 1.0)`). El `resolve()` de inputs internos también dejó de caer a `last_valid_out` y ahora emite `vec4(0,0,0,1)` para slots sin conectar. Tests: `dag_compile` 6/6 → **7/7** (test 4b verifica que el seed final aparece después de las branches de preview, no antes).
- **Generated GLSL autocontenido** (`compile_dag_to_glsl_baked`, nuevo en `dag_compile.{h,cpp}`):
- Sustituye `uniform vec4 u_params[64]` por `const vec4 u_params[N] = vec4[N](vec4(...), ...)` con los valores actuales del pipeline empaquetados (mismo layout que `dag_uniforms_apply`).
- Sustituye `uniform int u_preview_target` por `const int u_preview_target = -1` (las branches de preview quedan muertas y el GLSL compiler las elimina).
- Resultado: el shader del panel `Generated GLSL` no depende de ningún uniform externo. Pegarlo en el editor `Code` reproduce exactamente el render del DAG en el momento del copy. Después editar el DAG no afecta al Code.
- Test 7 nuevo: `dag_compile_baked` no contiene `uniform vec4 u_params` ni `uniform int u_preview_target`, sí contiene `const vec4 u_params[` y los valores empaquetados.
- **Importante**: el `Canvas Code` ya NO recibe `dag_uniforms_apply`. Es totalmente independiente. (Versión anterior intentaba sincronizarlos; rompía el aislamiento entre paneles.)
- **`dag_uniforms_apply` también resetea `u_preview_target = -1`** al final, para que la rama de preview quede desactivada en el render principal del Canvas DAG. La rutina `dag_previews_render` la activa de forma transitoria por nodo y la deja restaurada.
- **Drop-replace del mismo kind**:
- Soltar un nodo de la paleta sobre un nodo existente del **mismo `DagKind`** (Gen sobre Gen, Op sobre Op, Blend sobre Blend, nunca sobre Output) sustituye `name`+`params`+`controls` conservando `id`, `editor_uid`, `editor_pos_x/y`, `source_ids[]` y `preview_open`.
- Slots de input que sobran (si el nuevo def tiene menos `num_inputs` que el anterior) se limpian.
- Hit-test contra cajas de nodos vía `ed::GetNodePosition` + `ed::GetNodeSize` (canvas-space). No se usa `ed::GetHoveredNode()` porque no es fiable durante un drag-drop activo.
- **Drop-on-cable splice (intercalar nodo)**:
- Soltar un nodo de la paleta **o** arrastrar un nodo Op/Blend ya existente sobre un cable: el nodo se inserta entre `src` y `dst`. `new.source_ids[0] = src.id`, `dst.source_ids[slot] = new.id`. Para Blend (2 inputs), slot 0 queda cableado y slot 1 vacío.
- Para nodos existentes movidos: además de las dos rewires anteriores, se limpian todas las refs hacia el nodo movido en otros `source_ids[]` antes (lo desengancha de cualquier consumidor previo, queda exclusivamente en la nueva posición). Tracking del nodo arrastrado vía `s_drag_existing_uid` (set en `IsMouseClicked(0)` cuando hay un nodo hovered y no hay pin hovered, def es Gen/Op/Blend, no Output).
- Hit-test del cable: distancia punto-segmento (`dist_point_to_segment`) entre el cursor y la línea aproximada `(src.right_mid → dst.left_at_slot_k)`. Threshold **18 px** canvas-space.
- Prioridad: cable-hit > node-hit > add-vacío.
- **Splice highlight (preview visual)**:
- Mientras hay un drag activo de paleta o de nodo del canvas, el cable candidato se redibuja en `SPLICE_COLOR = (1.00, 0.82, 0.18, 1)` (dorado) más grueso (`CABLE_THICK + 2`).
- **Garantía visual**: además de cambiar el color en `ed::Link()`, se dibuja un bezier dorado encima en el `ImGui::GetForegroundDrawList()` (canvas → screen via `ed::CanvasToScreen`). Esto evita problemas de compositing interno del editor que podían enterrar el cambio de color.
- Detección sin gates: la versión anterior gateaba con `IsMouseDown` + `window_hovered`, lo que silenciaba el highlight. Ahora basta con la presencia del payload de drag-drop (paleta) o del `s_drag_existing_uid` (nodo del canvas).
- **Catalog `dag_catalog.cpp`** ya soporta `is_builtin` (Fase 5) y permite `dag_register_node` / `dag_unregister_node` para generators custom; el splice/replace funciona sobre todos por igual (Built-ins, Gen custom guardados desde Code).
Comandos:
```bash
# Build linux
./fn run build_cpp_linux_bash_infra shaders_lab
# Build windows (cross-compile)
./fn run build_cpp_windows_bash_infra shaders_lab
# Tests del dominio gfx (puros, sin GL)
g++ -std=c++17 -Icpp/functions -DDAG_CATALOG_TEST cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_catalog_test && /tmp/dag_catalog_test
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 && /tmp/dag_compile_test
g++ -std=c++17 -Icpp/functions -DCODE_TO_GENERATOR_TEST cpp/functions/gfx/code_to_generator.cpp cpp/functions/gfx/uniform_parser.cpp -o /tmp/code_to_generator_test && /tmp/code_to_generator_test
g++ -std=c++17 -Icpp/functions -DUNIFORM_PARSER_TEST cpp/functions/gfx/uniform_parser.cpp -o /tmp/uniform_parser_test && /tmp/uniform_parser_test
gcc -c -O2 -DSQLITE_THREADSAFE=1 cpp/vendor/sqlite3/sqlite3.c -o /tmp/sqlite3.o && \
g++ -std=c++17 -Icpp/functions -Icpp/vendor/sqlite3 -DSHADERLAB_DB_TEST cpp/functions/gfx/shaderlab_db.cpp /tmp/sqlite3.o -lpthread -ldl -o /tmp/shaderlab_db_test && /tmp/shaderlab_db_test
```
Cobertura de tests inline tras esta fase: **8 + 7 + 7 + 6 + 7 = 35 asserts** sobre `dag_catalog` (19 nodos), `dag_compile` (strict + baked), `code_to_generator`, `uniform_parser`, `shaderlab_db`.
Sync de binarios Windows (regla establecida en esta sesión):
- `cpp/build/windows/apps/shaders_lab/shaders_lab.exe` (origen)
- `apps/shaders_lab/shaders_lab.exe` (in-repo)
- `/mnt/c/Users/lucas/Desktop/shaders_lab.exe` (Windows Desktop)
- **NUNCA** copiar a `/mnt/c/Users/AdminLocal/`. Memoria persistente: `feedback_no_adminlocal.md`.
## Lo siguiente que pega
- Push selectivo al registry global: boton `Push to registry` que extrae el generator a `cpp/functions/gfx/<name>.{cpp,md}` con tag `shaders_lab` y dispara `fn index`.
- Listado / borrado de generators custom desde la UI (hoy solo via DB directa).
- Persistencia de pipelines con nombre.
- Mas nodos: warps (twirl, polar, kaleidoscope), perlin/fbm reales, SDFs, filtros de luma.
- Save as Op (1 input `a`) y Save as Blend (2 inputs).
- Crossfade A↔B: tercer canvas que mezcla Canvas Code y Canvas DAG con un slider.
- Cliente Claude: chat con tool use (`search_registry`, `apply_shader`, `save_function`).
- Integracion VJ: Spout/Syphon/NDI para mandar el output a Resolume/OBS.
Documentacion de exploraciones aparcadas (no en backlog inmediato):
- `NEXT_STEPS_DATA_TYPES.md` — extensiones del DAG: pins tipados, texturas, SDF/raymarch, multi-pass, geometria 3D.
- `NEXT_STEPS_BORDERLESS_WINDOW.md` — quitar la titlebar del SO y mover min/max/close al `MainMenuBar` ImGui.
## Build ## Build
```bash ```bash
./fn run build_cpp_linux_bash_infra shaders_lab # Linux
./fn run build_cpp_windows_bash_infra shaders_lab 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
``` ```
- Linux: `cpp/build/linux/apps/shaders_lab/shaders_lab` ## Decisiones
- Windows (cross-compile mingw-w64): `cpp/build/windows/apps/shaders_lab/shaders_lab.exe` — copiado a `apps/shaders_lab/shaders_lab.exe` y al Desktop tras cada build.
## Tests - `init_gl_loader = true` (via `fn::run_app` por default cuando se enlaza
con OpenGL) — `shader_canvas`, `gl_shader`, `gl_framebuffer` llaman gl*.
```bash - `viewports = true` — los Canvas se pueden arrastrar fuera del main.
g++ -std=c++17 -Icpp/functions -DUNIFORM_PARSER_TEST cpp/functions/gfx/uniform_parser.cpp -o /tmp/uniform_parser_test && /tmp/uniform_parser_test - DAG default: arranca con un nodo "plasma" + "output" si la paleta los
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 && /tmp/dag_compile_test encuentra; persiste el INI con `layout_storage`.
g++ -std=c++17 -Icpp/functions -DDAG_CATALOG_TEST cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_catalog_test && /tmp/dag_catalog_test - 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`).
Cobertura actual: 6 + 6 + 8 = 20 asserts puros (sin GL/ImGui). La parte UI (`dag_node_editor`, `dag_palette`, `uniform_panel`, `shader_canvas`) no es testeable sin entorno grafico.
## Uniforms del Code mode
Auto-prependidos por `compile_fragment`:
```glsl
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
out vec4 fragColor;
```
El cuerpo del fragment se escribe sin `#version`, sin `out`, sin esos uniforms. Cualquier `uniform` adicional declarado por el usuario con anotacion (`// @slider`, etc.) genera un widget en `Controls`.
## Uniforms del DAG mode
Ademas de los anteriores, el shader generado por `compile_dag_to_glsl` declara:
```glsl
uniform vec4 u_params[16]; // 4 floats por nodo (slot del nodo i en u_params[i])
uniform int u_preview_target; // -1 = real Output; 0..15 = render out_<i>
```
`dag_uniforms_apply` sube `u_params[16]` cada frame antes del draw del Canvas DAG. `dag_previews_render` rebinde el FBO de cada nodo abierto y setea `u_preview_target` antes de cada draw.
## Layouts
ImGui persiste el layout actual en `imgui.ini` junto al binario (autosave). Ademas, el menu **Layouts** permite tener varios layouts guardados con nombre:
- Mueve los paneles donde quieras.
- `Layouts > Save current as...` y dale un nombre (ej. "Coding", "DAG mode", "Showcase").
- Cambia el layout, guarda otro.
- `Layouts > <nombre>` para saltar; el activo se marca con `* `.
- `Layouts > Delete > <nombre>` para borrar.
- `Layouts > Reset to default` reabre todos los paneles y limpia el marker.
Los layouts guardados viven en la tabla `ui_layouts` de `shaders_lab.db`.
Disposicion comoda al primer arranque:
- `Code` y `DAG Pipeline` ocupan la fila superior.
- `Canvas Code` y `Canvas DAG` ocupan la fila inferior, lado a lado.
- `Functions` y `Controls` van a un lateral.
- `Generated GLSL` minimizado o en pestana junto a `Controls`.
El menu **View** togglea cada panel individualmente (mismo `bool*` que la X de la ventana).
## Notas de cross-compile
- `gl_loader` resuelve simbolos OpenGL 2.0+ con `wglGetProcAddress` en Windows; en Linux es no-op (`GL_GLEXT_PROTOTYPES`).
- `WIN32_EXECUTABLE TRUE` en `CMakeLists.txt` evita la consola al lanzar el .exe.
- Vendor de imgui-node-editor cuesta ~1MB en el binario final (~18 MB total).
## Notas — Settings + iconos (sesion 2026-04-25)
- `app_menubar` ahora añade automaticamente un tercer item `Settings...` junto a `View` y `Layouts`. Click abre la ventana flotante de `app_settings` (Display: toggle FPS overlay; Typography: combo de fuente Karla/Roboto/DroidSans/Cousine + slider de tamaño 10..32 px). Persiste en `app_settings.ini` junto a `shaders_lab.exe`.
- Defaults: DroidSans 15 px, FPS overlay off (antes hardcoded ON dentro del panel `Controls`).
- Removida la llamada explicita `fps_overlay()` del panel `Controls` — ahora se respeta el toggle de Settings.
- Removidos los `.cpp` de `fps_overlay`, `panel_menu`, `layouts_menu`, `app_menubar` del `CMakeLists.txt` — viven en `fn_framework` para evitar multiple-definition. Solo `layout_storage_sqlite.cpp` sigue listado explicitamente.
- 5 TTFs (Karla / Roboto / DroidSans / Cousine / Tabler) copiadas junto al exe via `add_imgui_app` post-build.
Para añadir secciones propias de settings:
```cpp
// En main.cpp antes de fn::run_app:
fn_ui::settings_window_add_section("shader_compiler", "Shader compiler", []{
ImGui::Checkbox("Auto-compile on save", &g_auto_compile);
ImGui::SliderInt("Debounce (ms)", &g_debounce_ms, 50, 2000);
});
// Aparece debajo de Display/Typography. Persistencia propia (puede ir en
// shaders_lab.db, tabla ui_settings).
```
## Lo siguiente que pega
- Ejemplo concreto de seccion extra de settings: `auto-compile on save` + `debounce_ms` registrados desde `main.cpp` y persistidos en una tabla `ui_settings` en `shaders_lab.db`.
- Auditar hex UTF-8 (`"\x..\x.."`) o emojis Unicode hardcoded en uniform_panel, dag_panel, dag_node_editor → migrar a `TI_*` de `core/icons_tabler.h`.
- Rebuild Windows + sync: `cmake --build cpp/build/windows --target shaders_lab && cp cpp/build/windows/apps/shaders_lab/{shaders_lab.exe,*.ttf} /mnt/c/Users/lucas/Desktop/apps/shaders_lab/`.
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "analyze_dns(domain: string, mode: string) -> void" signature: "analyze_dns(domain: string, mode: string) -> void"
description: "Análisis DNS completo de un dominio: registros A/AAAA/MX/NS/TXT/CNAME/SOA, consulta whois y verificación contra listas negras DNSBL (spamhaus, spamcop, sorbs, barracuda)." description: "Análisis DNS completo de un dominio: registros A/AAAA/MX/NS/TXT/CNAME/SOA, consulta whois y verificación contra listas negras DNSBL (spamhaus, spamcop, sorbs, barracuda)."
tags: [bash, cybersecurity, dns, network, whois, dnsbl, reconnaissance] tags: [bash, cybersecurity, dns, network, whois, dnsbl, reconnaissance, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "audit_http_headers(url: string) -> void" signature: "audit_http_headers(url: string) -> void"
description: "Audita las cabeceras HTTP de seguridad de una URL: verifica la presencia de HSTS (con validación de max-age mínimo de 6 meses), Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy y cabeceras CORS. También detecta cabeceras que exponen información del servidor." description: "Audita las cabeceras HTTP de seguridad de una URL: verifica la presencia de HSTS (con validación de max-age mínimo de 6 meses), Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy y cabeceras CORS. También detecta cabeceras que exponen información del servidor."
tags: [bash, cybersecurity, web, http, headers, security, hsts, csp, hardening] tags: [bash, cybersecurity, web, http, headers, security, hsts, csp, hardening, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "audit_ssh_config(config_path: string) -> void" signature: "audit_ssh_config(config_path: string) -> void"
description: "Audita la configuración de sshd_config evaluando parámetros de seguridad críticos (PermitRootLogin, PasswordAuthentication, Port, MaxAuthTries, X11Forwarding, AllowUsers). También revisa intentos de login fallidos en los logs y lista las claves autorizadas del usuario actual." description: "Audita la configuración de sshd_config evaluando parámetros de seguridad críticos (PermitRootLogin, PasswordAuthentication, Port, MaxAuthTries, X11Forwarding, AllowUsers). También revisa intentos de login fallidos en los logs y lista las claves autorizadas del usuario actual."
tags: [bash, cybersecurity, ssh, audit, security, hardening, linux] tags: [bash, cybersecurity, ssh, audit, security, hardening, linux, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "check_firewall() -> void" signature: "check_firewall() -> void"
description: "Detecta el firewall activo del sistema (ufw, firewalld o iptables) y muestra su estado, reglas activas y puertos en escucha para cruzar con las reglas. Si no se detecta ningún firewall, emite una advertencia de exposición." description: "Detecta el firewall activo del sistema (ufw, firewalld o iptables) y muestra su estado, reglas activas y puertos en escucha para cruzar con las reglas. Si no se detecta ningún firewall, emite una advertencia de exposición."
tags: [bash, cybersecurity, firewall, ufw, iptables, network, hardening, linux] tags: [bash, cybersecurity, firewall, ufw, iptables, network, hardening, linux, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "detect_suspicious_users() -> void" signature: "detect_suspicious_users() -> void"
description: "Revisa el sistema en busca de indicadores de compromiso en cuentas de usuario: UIDs 0 extras (además de root), usuarios con shell de login válida, homes en rutas inusuales, miembros de grupos privilegiados (sudo, docker, wheel, adm, etc.) y sesiones activas." description: "Revisa el sistema en busca de indicadores de compromiso en cuentas de usuario: UIDs 0 extras (además de root), usuarios con shell de login válida, homes en rutas inusuales, miembros de grupos privilegiados (sudo, docker, wheel, adm, etc.) y sesiones activas."
tags: [bash, cybersecurity, users, audit, linux, privilege-escalation, hardening] tags: [bash, cybersecurity, users, audit, linux, privilege-escalation, hardening, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "encrypt_file(mode: string, file: string) -> void" signature: "encrypt_file(mode: string, file: string) -> void"
description: "Cifra o descifra un archivo usando AES-256-CBC con PBKDF2 (310.000 iteraciones) via openssl. La contraseña se lee de la variable de entorno ENCRYPT_PASSWORD o se solicita interactivamente. El archivo cifrado se guarda con extensión .enc; al descifrar se recupera el nombre original." description: "Cifra o descifra un archivo usando AES-256-CBC con PBKDF2 (310.000 iteraciones) via openssl. La contraseña se lee de la variable de entorno ENCRYPT_PASSWORD o se solicita interactivamente. El archivo cifrado se guarda con extensión .enc; al descifrar se recupera el nombre original."
tags: [bash, cybersecurity, encryption, aes256, openssl, crypto, pbkdf2] tags: [bash, cybersecurity, encryption, aes256, openssl, crypto, pbkdf2, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "enumerate_subdomains(domain: string, output_file: string) -> void" signature: "enumerate_subdomains(domain: string, output_file: string) -> void"
description: "Enumera subdominios de un dominio objetivo usando un diccionario integrado de ~100 subdominios comunes (www, mail, api, dev, admin, vpn, etc.). Detecta tanto registros A (IP directa) como CNAME. Muestra progreso cada 20 subdominios y opcionalmente guarda los resultados en un archivo." description: "Enumera subdominios de un dominio objetivo usando un diccionario integrado de ~100 subdominios comunes (www, mail, api, dev, admin, vpn, etc.). Detecta tanto registros A (IP directa) como CNAME. Muestra progreso cada 20 subdominios y opcionalmente guarda los resultados en un archivo."
tags: [bash, cybersecurity, dns, subdomain, enumeration, reconnaissance, osint] tags: [bash, cybersecurity, dns, subdomain, enumeration, reconnaissance, osint, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "generate_password(mode: string, length: int, count: int) -> void" signature: "generate_password(mode: string, length: int, count: int) -> void"
description: "Genera contraseñas seguras en cuatro modos: full (alfanumérico + símbolos, excluye caracteres ambiguos), alpha (solo alfanumérico), passphrase (palabras aleatorias unidas con guión) y pin (numérico). Calcula y muestra la entropía en bits para cada modo." description: "Genera contraseñas seguras en cuatro modos: full (alfanumérico + símbolos, excluye caracteres ambiguos), alpha (solo alfanumérico), passphrase (palabras aleatorias unidas con guión) y pin (numérico). Calcula y muestra la entropía en bits para cada modo."
tags: [bash, cybersecurity, password, generator, entropy, security, urandom] tags: [bash, cybersecurity, password, generator, entropy, security, urandom, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "geolocate_ip(target: string) -> void" signature: "geolocate_ip(target: string) -> void"
description: "Geolocaliza una dirección IP o dominio usando la API pública de ip-api.com. Muestra país, región, ciudad, coordenadas, ISP, ASN y detecta VPN, Proxy o infraestructura de hosting." description: "Geolocaliza una dirección IP o dominio usando la API pública de ip-api.com. Muestra país, región, ciudad, coordenadas, ISP, ASN y detecta VPN, Proxy o infraestructura de hosting."
tags: [bash, cybersecurity, network, geoip, ip, osint, reconnaissance] tags: [bash, cybersecurity, network, geoip, ip, osint, reconnaissance, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "inspect_ssl_cert(host: string) -> void" signature: "inspect_ssl_cert(host: string) -> void"
description: "Inspecciona el certificado SSL/TLS de un host: muestra sujeto, emisor, fechas de validez, días hasta expiración, SANs (Subject Alternative Names), cadena de confianza completa y detecta soporte de versiones inseguras TLS 1.0/1.1." description: "Inspecciona el certificado SSL/TLS de un host: muestra sujeto, emisor, fechas de validez, días hasta expiración, SANs (Subject Alternative Names), cadena de confianza completa y detecta soporte de versiones inseguras TLS 1.0/1.1."
tags: [bash, cybersecurity, ssl, tls, certificate, web, openssl, security] tags: [bash, cybersecurity, ssl, tls, certificate, web, openssl, security, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "list_active_connections(mode: string) -> void" signature: "list_active_connections(mode: string) -> void"
description: "Muestra conexiones de red activas del sistema usando ss: puertos en escucha, conexiones establecidas y detección de conexiones hacia IPs externas (excluye RFC1918, loopback y link-local)." description: "Muestra conexiones de red activas del sistema usando ss: puertos en escucha, conexiones establecidas y detección de conexiones hacia IPs externas (excluye RFC1918, loopback y link-local)."
tags: [bash, cybersecurity, network, connections, monitoring, ss, ports] tags: [bash, cybersecurity, network, connections, monitoring, ss, ports, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -0,0 +1,56 @@
---
name: scan_secrets_in_dirty
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "scan_secrets_in_dirty(repo_dir: string) -> stdout: matched paths"
description: "Para un repo git, lista archivos modificados/nuevos cuyo nombre matchee patron de secret. Patrones: .env, credentials, .key, .pem, id_rsa, secret, token*.txt. Stdout vacio si no hay matches. Exit 0 siempre que el repo exista."
tags: [git, secrets, security, scan, credentials, cybersecurity]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: repo_dir
desc: "path al repo git a escanear; default '.'"
output: "paths sospechosos por stdout (uno por linea), vacio si todo limpio; exit 1 solo si repo_dir no es un repo git"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/scan_secrets_in_dirty.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/scan_secrets_in_dirty.sh
# Escanear repo actual
matches=$(scan_secrets_in_dirty .)
if [[ -n "$matches" ]]; then
echo "ABORTAR: archivos sospechosos detectados:"
echo "$matches"
exit 1
fi
# Escanear repo especifico
scan_secrets_in_dirty /home/lucas/fn_registry
```
## Patrones detectados
- `.env`, `.env.local`, `.env.production`, etc.
- `*credentials*`
- `*.key`
- `*.pem`
- `id_rsa*`
- `*secret*`
- `*token*.txt`
## Notas
Usa `git status --porcelain` para listar solo archivos del working tree (modificados, nuevos, staged). No escanea el contenido del archivo, solo el nombre. Las claves GPG cifradas (`.gpg`) no se detectan intencionalmente — son opacas. Exit 0 siempre que el directorio sea un repo git valido, incluso si no hay matches.
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# scan_secrets_in_dirty — Para un repo git, lista archivos modificados/nuevos
# cuyo nombre matchee patron de secret. Stdout vacio si no hay matches.
# Exit 0 siempre que el repo exista (el caller decide si abortar).
scan_secrets_in_dirty() {
local repo_dir="${1:-.}"
if [[ ! -d "$repo_dir/.git" ]]; then
echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
return 1
fi
# Listar archivos modificados o nuevos (excluyendo borrados)
# y filtrar por patron de secret en el nombre del archivo
git -C "$repo_dir" status --porcelain \
| awk '{print $NF}' \
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
|| true
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
scan_secrets_in_dirty "$@"
fi
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "verify_file_hash(file: string, algorithm: string, expected_hash: string) -> void" signature: "verify_file_hash(file: string, algorithm: string, expected_hash: string) -> void"
description: "Calcula el hash criptográfico de un archivo con el algoritmo especificado (md5, sha1, sha256, sha512) y opcionalmente lo compara con un hash esperado para verificar integridad. Retorna exit code 1 si los hashes no coinciden." description: "Calcula el hash criptográfico de un archivo con el algoritmo especificado (md5, sha1, sha256, sha512) y opcionalmente lo compara con un hash esperado para verificar integridad. Retorna exit code 1 si los hashes no coinciden."
tags: [bash, cybersecurity, hash, integrity, checksum, md5, sha256, sha512] tags: [bash, cybersecurity, hash, integrity, checksum, md5, sha256, sha512, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
+98
View File
@@ -0,0 +1,98 @@
---
name: adb_wsl
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "source adb_wsl.sh [ADB=<path>] [ANDROID_SDK_WIN=<sdk_root>]"
description: "Wrapper sourceable para usar adb.exe Windows desde WSL2. Resuelve binario, convierte paths, espera boot del emulador."
tags: ["android", "adb", "wsl", "windows"]
params:
- name: ADB
desc: "Env var opcional. Path absoluto a adb.exe. Si no se fija, se construye desde ANDROID_SDK_WIN o el default /mnt/c/Users/lucas/AppData/Local/Android/Sdk."
- name: ANDROID_SDK_WIN
desc: "Env var opcional. Raiz del Android SDK montado en WSL. Default: /mnt/c/Users/lucas/AppData/Local/Android/Sdk."
output: "Source-able shell helpers: adb_run, adb_devices, adb_wsl_to_win, adb_wait_boot. Define ADB env var apuntando a Windows adb.exe via ANDROID_SDK_WIN."
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/adb_wsl.sh"
---
## Uso
```bash
# Sourcear (usa SDK default)
source bash/functions/infra/adb_wsl.sh
# Sourcear con SDK custom
ANDROID_SDK_WIN=/mnt/d/Android/Sdk source bash/functions/infra/adb_wsl.sh
# Sourcear con binario fijo
ADB=/mnt/c/my/tools/adb.exe source bash/functions/infra/adb_wsl.sh
```
## Funciones expuestas
### `adb_run "<args...>"`
Ejecuta `$ADB` con los argumentos dados. Retorna el exit code de `adb.exe`.
```bash
adb_run shell ls /sdcard/
adb_run install app.apk
```
### `adb_devices`
Alias de `adb_run devices`. Lista dispositivos/emuladores conectados.
```bash
adb_devices
# List of devices attached
# emulator-5554 device
```
### `adb_wsl_to_win <path_wsl>`
Convierte un path WSL a formato Windows con `wslpath -w`. Si `wslpath` no está disponible retorna el path sin convertir.
```bash
win_path=$(adb_wsl_to_win /home/lucas/proyecto/app.apk)
# C:\Users\lucas\AppData\Local\... (o la ruta Windows equivalente)
adb_run install "$win_path"
```
### `adb_wait_boot [timeout_s]`
Espera a que el emulador/dispositivo complete el boot (`sys.boot_completed = 1`). Útil tras lanzar un AVD en CI.
```bash
adb_wait_boot # timeout 120s
adb_wait_boot 60 # timeout 60s
```
Retorna `0` si el boot se completó, `1` si expiró el timeout.
## Smoke test
```bash
bash bash/functions/infra/adb_wsl.sh --self-test
# OK
```
## Notas
- El script es **source-able**: define funciones en el shell actual, no crea subshell.
- `ADB` se resuelve una sola vez al sourcing. Si el binario no existe en disco, la carga falla con mensaje en stderr y `return 1` / `exit 1`.
- `adb_wait_boot` hace polling cada 3 segundos. Ajustar `interval` si el emulador es especialmente lento.
- En WSL2 `wslpath` siempre está disponible; el fallback existe para entornos Linux puros que accidentalmente sourceen el archivo.
- Si el emulador requiere `-s <serial>`, pasar el flag directamente a `adb_run`: `adb_run -s emulator-5554 shell ...`.
---
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# adb_wsl — Wrapper sourceable para usar adb.exe Windows desde WSL2.
# Uso: source bash/functions/infra/adb_wsl.sh
# Smoke test: bash bash/functions/infra/adb_wsl.sh --self-test
# ---------------------------------------------------------------------------
# Resolver ADB
# ---------------------------------------------------------------------------
# El caller puede fijar ADB antes de sourcing para apuntar a otro binario.
if [[ -z "${ADB:-}" ]]; then
_sdk_root="${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}"
ADB="${_sdk_root}/platform-tools/adb.exe"
unset _sdk_root
fi
if [[ ! -f "$ADB" ]]; then
echo "adb_wsl: ADB no encontrado en '$ADB'. Fija ADB= o ANDROID_SDK_WIN= antes de sourcear." >&2
# Solo abortamos si el script se ejecuta directamente; si se sourcea,
# permitimos continuar para que el caller maneje el error.
return 1 2>/dev/null || exit 1
fi
# ---------------------------------------------------------------------------
# adb_run "<args...>"
# Ejecuta el ADB Windows con los argumentos dados.
# Retorna el exit code de adb.exe.
# ---------------------------------------------------------------------------
adb_run() {
"$ADB" "$@"
}
# ---------------------------------------------------------------------------
# adb_devices
# Lista dispositivos ADB conectados.
# ---------------------------------------------------------------------------
adb_devices() {
adb_run devices
}
# ---------------------------------------------------------------------------
# adb_wsl_to_win <path_wsl>
# Convierte un path WSL a formato Windows usando wslpath.
# Si wslpath no está disponible retorna el path tal cual.
# ---------------------------------------------------------------------------
adb_wsl_to_win() {
local path_wsl="$1"
if command -v wslpath &>/dev/null; then
wslpath -w "$path_wsl"
else
echo "$path_wsl"
fi
}
# ---------------------------------------------------------------------------
# adb_wait_boot [timeout_s]
# Espera a que el dispositivo/emulador complete el boot (sys.boot_completed=1).
# timeout_s: segundos máximos de espera (default 120).
# Retorna 0 si boot completado, 1 si timeout.
# ---------------------------------------------------------------------------
adb_wait_boot() {
local timeout_s="${1:-120}"
local elapsed=0
local interval=3
while (( elapsed < timeout_s )); do
local val
val=$(adb_run shell getprop sys.boot_completed 2>/dev/null | tr -d '[:space:]')
if [[ "$val" == "1" ]]; then
return 0
fi
sleep "$interval"
(( elapsed += interval ))
done
echo "adb_wsl: timeout ${timeout_s}s esperando boot del dispositivo." >&2
return 1
}
# ---------------------------------------------------------------------------
# adb_pick_serial [--serial <S>] [...]
# Resuelve el serial a usar para multi-device. Lee --serial X de los args.
# Setea globals ADB_PICK_SERIAL y ADB_PICK_REST (no usa stdout para evitar
# perder los globals via subshell de $()).
# Exit 1 si no hay device disponible.
#
# Uso tipico:
# adb_pick_serial "$@" || { echo "no device" >&2; exit 3; }
# local serial="$ADB_PICK_SERIAL"
# set -- "${ADB_PICK_REST[@]}"
# ---------------------------------------------------------------------------
adb_pick_serial() {
ADB_PICK_SERIAL="${ADB_SERIAL:-}"
ADB_PICK_REST=()
while [[ $# -gt 0 ]]; do
case "$1" in
--serial) ADB_PICK_SERIAL="$2"; shift 2 ;;
--serial=*) ADB_PICK_SERIAL="${1#--serial=}"; shift ;;
*) ADB_PICK_REST+=("$1"); shift ;;
esac
done
if [[ -z "$ADB_PICK_SERIAL" ]]; then
ADB_PICK_SERIAL=$(adb_run devices 2>/dev/null | awk '/(emulator-|device$)/ && !/List of/ {print $1; exit}')
fi
if [[ -z "$ADB_PICK_SERIAL" ]]; then
echo "adb_wsl: ningun device/emulador conectado." >&2
return 1
fi
if ! adb_run devices 2>/dev/null | awk '{print $1}' | grep -qx "$ADB_PICK_SERIAL"; then
echo "adb_wsl: serial '$ADB_PICK_SERIAL' no encontrado en adb devices." >&2
return 1
fi
return 0
}
# ---------------------------------------------------------------------------
# adb_s <serial> <args...>
# Atajo: adb_run -s <serial> <args...>
# ---------------------------------------------------------------------------
adb_s() {
local serial="$1"; shift
adb_run -s "$serial" "$@"
}
# ---------------------------------------------------------------------------
# Smoke test (solo si invocado directamente con --self-test)
# ---------------------------------------------------------------------------
if [[ "${1:-}" == "--self-test" ]]; then
adb_run version || exit 1
echo "OK"
fi
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure purity: impure
signature: "analyze_disk_space([target_dir: string], [mode: string]) -> void" signature: "analyze_disk_space([target_dir: string], [mode: string]) -> void"
description: "Analiza el uso de espacio en disco. Modos: partitions (df con filtros), top-dirs (du top 10), top-files (find top 20), inodes (df -i), all (todos). Emite advertencias si el uso supera el 90%." description: "Analiza el uso de espacio en disco. Modos: partitions (df con filtros), top-dirs (du top 10), top-files (find top 20), inodes (df -i), all (todos). Emite advertencias si el uso supera el 90%."
tags: [bash, disk, space, analysis, filesystem] tags: [bash, disk, space, analysis, filesystem, pendiente-usar]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -0,0 +1,66 @@
---
name: android_apk_install
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_apk_install([--serial S], apk_path: string, package_name?: string, activity_name?: string) -> void"
description: "Instala APK en device/emulador via adb y opcionalmente lanza la app. Multi-emulator via --serial."
tags: [android, adb, apk, wsl]
params:
- name: "--serial <S>"
desc: "Optional target device/emulator serial. Default: first device detected by adb_pick_serial."
- name: apk_path
desc: "WSL path to APK file"
- name: package_name
desc: "Optional app package id (e.g. com.fnregistry.voiceguide). Launches the app if provided."
- name: activity_name
desc: "Optional activity (.MainActivity or fully qualified). Only used with package_name. If omitted, launches via monkey LAUNCHER intent."
output: "Stdout con pasos. Exit 0 = install + launch OK. Exit !=0 si install fallo o APK no encontrado."
uses_functions: ["adb_wsl_bash_infra"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_apk_install.sh"
---
## Ejemplo
```bash
# Solo instalar
android_apk_install /home/lucas/builds/app-debug.apk
# Instalar y lanzar con activity explícita
android_apk_install /home/lucas/builds/app-debug.apk com.fnregistry.voiceguide .MainActivity
# Instalar y lanzar sin activity (usa monkey LAUNCHER)
android_apk_install /home/lucas/builds/app-debug.apk com.fnregistry.voiceguide
# Llamada directa desde shell (no sourced)
bash bash/functions/infra/android_apk_install.sh /path/to/app.apk com.example.app .MainActivity
# Override ADB path
ADB=/custom/path/adb.exe bash bash/functions/infra/android_apk_install.sh /path/to/app.apk
```
## Notas
- Requiere WSL2 con `adb.exe` Windows accesible. El path por defecto es
`/mnt/c/Users/lucas/AppData/Local/Android/Sdk/platform-tools/adb.exe`.
Se puede sobreescribir con `ADB=...` o `ANDROID_SDK_WIN=<sdk_root>` antes
de invocar.
- `wslpath` se usa para convertir el path WSL a formato Windows (`C:\...`).
Si no está disponible (entorno no-WSL), se usa el path tal cual.
- La instalación usa `adb install -r` (reinstala si ya existe).
- Si `package_name` se da sin `activity_name`, la app se lanza via
`adb shell monkey -p <pkg> -c android.intent.category.LAUNCHER 1`,
que es equivalente a pulsar el icono del launcher.
- El script se puede sourcear (para usar la función en otros scripts) o
ejecutar directamente. Cuando se ejecuta directamente, delega en
`android_apk_install "$@"`.
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# android_apk_install — Instala APK en device/emulador via adb y opcionalmente lanza la app.
# Multi-emulator via --serial <S>.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source helpers (adb_run, adb_pick_serial, adb_s, adb_wsl_to_win)
# shellcheck source=/dev/null
source "$SCRIPT_DIR/adb_wsl.sh"
# ---------------------------------------------------------------------------
# android_apk_install [--serial <S>] <apk_path> [package_name] [activity_name]
# ---------------------------------------------------------------------------
android_apk_install() {
local serial
adb_pick_serial "$@" || { echo "android_apk_install: no device/emulator." >&2; return 3; }
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local apk="${1:-}"
local package="${2:-}"
local activity="${3:-}"
if [[ -z "$apk" ]]; then
echo "android_apk_install: se requiere apk_path como primer argumento." >&2
return 1
fi
if [[ ! -f "$apk" ]]; then
echo "android_apk_install: APK no encontrado en '$apk'." >&2
return 1
fi
local win_path
win_path=$(adb_wsl_to_win "$apk")
echo "android_apk_install: instalando '$win_path' on $serial ..."
adb_s "$serial" install -r "$win_path"
echo "android_apk_install: instalacion completada."
if [[ -n "$package" ]]; then
if [[ -n "$activity" ]]; then
echo "android_apk_install: lanzando $package/$activity ..."
adb_s "$serial" shell am start -n "$package/$activity"
else
echo "android_apk_install: lanzando $package via monkey LAUNCHER ..."
adb_s "$serial" shell monkey -p "$package" -c android.intent.category.LAUNCHER 1
fi
echo "android_apk_install: app lanzada."
fi
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_apk_install "$@"
fi
+52
View File
@@ -0,0 +1,52 @@
---
name: android_app_clear
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_app_clear([--serial <S>], package: string) -> void"
description: "Wipe app data + cache via pm clear. App keeps installed but factory-state. Multi-emulator via --serial."
tags: [android, adb, app, clear, reset, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional target device/emulator serial. Auto-detected if omitted."
- name: package
desc: "App package whose data to clear (e.g. com.example.app)."
output: "Stdout 'cleared data for <pkg> on <serial>'. Exit 0 si pm clear OK."
uses_functions: ["adb_wsl_bash_infra"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_app_clear.sh"
---
## Ejemplo
```bash
# Limpiar datos de una app (autodetecta device)
android_app_clear com.example.myapp
# Con serial explícito
android_app_clear --serial emulator-5554 com.example.myapp
# Llamada directa
bash bash/functions/infra/android_app_clear.sh com.example.myapp
bash bash/functions/infra/android_app_clear.sh --serial emulator-5554 com.example.myapp
```
## Notas
- Usa `pm clear` internamente — borra SharedPreferences, bases de datos internas,
caché y archivos de la app. La app queda como recién instalada.
- El source de `adb_wsl.sh` resuelve el binario `adb.exe` Windows desde WSL2.
Se puede sobreescribir con `ADB=...` o `ANDROID_SDK_WIN=<sdk_root>` antes de invocar.
- `adb_pick_serial` consume `--serial <S>` de los args y deja el resto en
`ADB_PICK_REST`. Si no se da, autodetecta el primer device/emulador activo.
- Exit 3 si no hay ningún device conectado (propagado desde `adb_pick_serial`).
- Exit 1 si no se pasa package.
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# android_app_clear — Wipe app data + cache via pm clear. App stays installed.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
# ---------------------------------------------------------------------------
# android_app_clear [--serial <S>] <package>
#
# --serial <S> Optional target device/emulator serial.
# package App package whose data+cache to clear (e.g. com.example.app).
#
# Calls: adb shell pm clear <package>
# The app remains installed but is reset to factory state (no data, no cache).
# Exit 0 on success, exit 1 on bad args, exit 3 if no device found.
# ---------------------------------------------------------------------------
android_app_clear() {
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local pkg="${1:-}"
if [[ -z "$pkg" ]]; then
echo "android_app_clear: se requiere <package> como argumento." >&2
return 1
fi
adb_s "$serial" shell pm clear "$pkg"
echo "cleared data for $pkg on $serial"
}
# Run directly if not sourced
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_app_clear "$@"
fi
+57
View File
@@ -0,0 +1,57 @@
---
name: android_app_info
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_app_info([--serial <S>], package, [--json]) -> stdout"
description: "Inspect installed app: version, target SDK, activities via dumpsys package."
tags: [android, adb, app, info, dumpsys, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional ADB serial to target a specific device/emulator. Auto-detected if omitted."
- name: "package"
desc: "Android package name to inspect (e.g. com.example.myapp)."
- name: "--json"
desc: "Emit parsed JSON with versionName, versionCode, targetSdk, launcherActivity instead of raw dumpsys output."
output: "Raw dumpsys package output, or JSON object {package, versionName, versionCode, targetSdk, launcherActivity}. Outputs JSON null if package not installed (--json mode). Exit 2 if package not found in raw mode, exit 3 if no device."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_app_info.sh"
---
## Ejemplo
```bash
# Raw dumpsys (full output)
source bash/functions/infra/android_app_info.sh
android_app_info com.example.myapp
# Target specific device
android_app_info --serial emulator-5554 com.example.myapp
# Parsed JSON
android_app_info com.example.myapp --json
# {"package":"com.example.myapp","versionName":"2.1.0","versionCode":210,"targetSdk":34,"launcherActivity":"com.example.myapp/.MainActivity"}
# Package not installed → JSON null
android_app_info com.not.installed --json
# null
```
## Notas
- Sources `adb_wsl.sh` para resolver el binario ADB Windows desde WSL2 y las helpers `adb_pick_serial` / `adb_s`.
- `--serial` se consume via `adb_pick_serial`; el resto de los args quedan en `ADB_PICK_REST` y se re-asignan con `set --`.
- JSON parsing usa `grep`/`sed`/`awk` sobre la salida de `dumpsys package`. Campos faltantes se emiten como string vacío o 0; no se usa `jq` para no requerir dependencias externas.
- `launcherActivity` se extrae buscando el bloque `android.intent.action.MAIN` / `android.intent.category.LAUNCHER` en el listado de intent filters.
- Exit codes: 0 = OK, 1 = arg/adb error, 2 = package not found (raw mode), 3 = no device.
---
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# android_app_info — Inspect installed app via dumpsys package.
# Usage: android_app_info [--serial <S>] <package> [--json]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/adb_wsl.sh"
android_app_info() {
# Resolve serial (consumes --serial from args, leaves rest in ADB_PICK_REST)
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
# Parse remaining args: package + --json flag
local pkg=""
local want_json=0
while [[ $# -gt 0 ]]; do
case "$1" in
--json) want_json=1; shift ;;
-*) echo "android_app_info: unknown flag '$1'" >&2; return 1 ;;
*)
if [[ -z "$pkg" ]]; then
pkg="$1"
else
echo "android_app_info: unexpected argument '$1'" >&2
return 1
fi
shift ;;
esac
done
if [[ -z "$pkg" ]]; then
echo "android_app_info: package argument required" >&2
return 1
fi
local dump
dump=$(adb_s "$serial" shell dumpsys package "$pkg" 2>&1)
local rc=$?
if [[ $rc -ne 0 ]]; then
echo "android_app_info: adb dumpsys failed (exit $rc)" >&2
return 1
fi
# If dumpsys returns nothing meaningful for the package, treat as not installed
if ! echo "$dump" | grep -q "Package \["; then
if [[ $want_json -eq 1 ]]; then
echo "null"
else
echo "android_app_info: package '$pkg' not found on device" >&2
return 2
fi
return 0
fi
if [[ $want_json -eq 0 ]]; then
echo "$dump"
return 0
fi
# --- JSON extraction ---
local versionName versionCode targetSdk launcherActivity
versionName=$(echo "$dump" | grep -m1 'versionName=' \
| sed 's/.*versionName=\([^ ]*\).*/\1/')
versionCode=$(echo "$dump" | grep -m1 'versionCode=' \
| sed 's/.*versionCode=\([0-9]*\).*/\1/')
targetSdk=$(echo "$dump" | grep -m1 'targetSdk=' \
| sed 's/.*targetSdk=\([0-9]*\).*/\1/')
# Primary/launcher activity: look for MAIN/LAUNCHER category block
launcherActivity=$(echo "$dump" | awk '
/android.intent.action.MAIN/ { found=1 }
found && /[a-zA-Z0-9_.]+\/[a-zA-Z0-9_.]+/ {
match($0, /[a-zA-Z0-9_.]+\/[a-zA-Z0-9_.]+/)
print substr($0, RSTART, RLENGTH)
exit
}
')
# Emit JSON, quoting strings safely
printf '{"package":"%s","versionName":"%s","versionCode":%s,"targetSdk":%s,"launcherActivity":"%s"}\n' \
"$pkg" \
"${versionName:-}" \
"${versionCode:-0}" \
"${targetSdk:-0}" \
"${launcherActivity:-}"
}
# Run if invoked directly
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
android_app_info "$@"
fi
+44
View File
@@ -0,0 +1,44 @@
---
name: android_app_kill
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_app_kill([--serial <S>], package: string) -> void"
description: "Force-stop running app via am force-stop. Multi-emulator via --serial."
tags: [android, adb, app, kill, force-stop, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional target device/emulator serial. Auto-detected if omitted."
- name: "package"
desc: "App package to force-stop (e.g. com.example.myapp)."
output: "Stdout 'killed <pkg> on <serial>'. Exit 0."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_app_kill.sh"
---
## Ejemplo
```bash
# Detener app en el emulador activo
android_app_kill com.example.myapp
# Detener app en un dispositivo concreto
android_app_kill --serial emulator-5554 com.example.myapp
```
## Notas
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el dispositivo objetivo.
Si `--serial` no se pasa, autodetecta el primer device/emulador disponible.
Sale con exit 3 si no hay ningun device conectado.
`am force-stop` detiene todos los procesos y servicios de la app de forma inmediata.
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# android_app_kill — Force-stop a running Android app via am force-stop.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
android_app_kill() {
local serial pkg
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
pkg="${1:?android_app_kill: package name required}"
adb_s "$serial" shell am force-stop "$pkg"
echo "killed $pkg on $serial"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_app_kill "$@"
fi
@@ -0,0 +1,49 @@
---
name: android_app_launch
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_app_launch([--serial <S>], package: string, [activity: string]) -> void"
description: "Launch app activity via am start. Multi-emulator via --serial."
tags: [android, adb, app, launch, activity, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional target serial. Default: first device"
- name: "package"
desc: "App package id"
- name: "activity"
desc: "Optional activity. If omitted, launches via LAUNCHER intent"
output: "Stdout 'launched <pkg> on <serial>'. Exit 0 ok, 3 no device."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_app_launch.sh"
---
## Ejemplo
```bash
# Lanzar actividad principal explicitamente
android_app_launch com.foo.bar .MainActivity
# Lanzar por LAUNCHER intent (detecta actividad principal automaticamente)
android_app_launch com.foo.bar
# Multi-emulador: elegir serial concreto
android_app_launch --serial emulator-5554 com.foo.bar .MainActivity
```
## Notas
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el serial objetivo.
Si no hay ningun device/emulador disponible, sale con exit code 3.
Si `activity` no se especifica, usa `monkey -p <pkg> -c android.intent.category.LAUNCHER 1`
para lanzar la actividad principal sin necesidad de conocerla de antemano.
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# android_app_launch — Launch an Android app via adb am start or monkey LAUNCHER intent.
# Usage: android_app_launch [--serial <S>] <package> [<activity>]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/adb_wsl.sh"
android_app_launch() {
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local pkg="${1:-}"
local activity="${2:-}"
if [[ -z "$pkg" ]]; then
echo "android_app_launch: package is required." >&2
return 1
fi
if [[ -n "$activity" ]]; then
adb_s "$serial" shell am start -n "$pkg/$activity"
else
adb_s "$serial" shell monkey -p "$pkg" -c android.intent.category.LAUNCHER 1
fi
echo "launched $pkg on $serial"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_app_launch "$@"
fi
@@ -0,0 +1,52 @@
---
name: android_app_uninstall
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_app_uninstall([--serial <S>] package [--keep-data]) -> void"
description: "Uninstall app via adb uninstall. Optionally keep data with --keep-data."
tags: [android, adb, app, uninstall, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional target device/emulator serial. Auto-detects first connected device if omitted."
- name: "package"
desc: "Android package name to uninstall (e.g. com.example.myapp). Mandatory positional argument."
- name: "--keep-data"
desc: "Keep app data + cache after uninstall (passes -k to pm uninstall)."
output: "Stdout 'uninstalled <pkg> on <serial>'. Exit 0 OK."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_app_uninstall.sh"
---
## Ejemplo
```bash
# Desinstalar en el device por defecto
android_app_uninstall com.example.myapp
# Desinstalar en un device concreto
android_app_uninstall --serial emulator-5554 com.example.myapp
# Desinstalar conservando datos y cache
android_app_uninstall --serial emulator-5554 com.example.myapp --keep-data
```
## Notas
Sourcea `adb_wsl.sh` para resolver el binario `adb.exe` en WSL2 y usar
`adb_pick_serial` / `adb_s`. Si no hay ningún device conectado y no se
pasa `--serial`, la función falla con exit 1 antes de invocar adb.
El flag `--keep-data` pasa `-k` a `adb uninstall`, equivalente a
`pm uninstall -k` — el APK se elimina pero los datos y la caché de la app
permanecen en el dispositivo.
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# android_app_uninstall — Desinstala una app Android via adb uninstall.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/adb_wsl.sh"
android_app_uninstall() {
# Parse --serial (consumes it, rest stays in ADB_PICK_REST)
local serial
adb_pick_serial "$@" || return 1
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
# Parse --keep-data flag
local keep_data=0
local args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--keep-data) keep_data=1; shift ;;
*) args+=("$1"); shift ;;
esac
done
set -- "${args[@]}"
local pkg="${1:-}"
if [[ -z "$pkg" ]]; then
echo "android_app_uninstall: package obligatorio." >&2
return 1
fi
if (( keep_data )); then
adb_s "$serial" uninstall -k "$pkg" || return 1
else
adb_s "$serial" uninstall "$pkg" || return 1
fi
echo "uninstalled $pkg on $serial"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_app_uninstall "$@"
fi
@@ -0,0 +1,51 @@
---
name: android_emu_battery
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_emu_battery([--serial <S>], level: int, [--charging <true|false>]) -> void"
description: "Simulate battery state on emulator (level + charging). Emulator-only."
tags: [android, emulator, battery, power, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional emulator serial (e.g. emulator-5554). Auto-detected if omitted."
- name: "level"
desc: "Battery level 0-100 to set via 'emu power capacity'."
- name: "--charging <true|false>"
desc: "AC charging state: true maps to 'on', false maps to 'off'. Omit to leave unchanged."
output: "Stdout 'battery: <N>% [charging=...] on <serial>'. Exit 3 if no device found, exit 1 on other errors."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_emu_battery.sh"
notes: "Util para tests bateria baja, modo ahorro energia. Solo funciona con emuladores (serial emulator-*), no con devices fisicos."
---
## Ejemplo
```bash
# Nivel al 15%, sin cambiar estado de carga
android_emu_battery 15
# Nivel al 5%, forzar descarga (AC off)
android_emu_battery 5 --charging false
# Nivel al 80%, forzar carga (AC on), emulador concreto
android_emu_battery --serial emulator-5554 80 --charging true
```
## Notas
Util para tests de bateria baja y modo ahorro de energia. Solo funciona con emuladores Android
(serial debe empezar con `emulator-`). No aplica a dispositivos fisicos.
Requiere que `adb_wsl.sh` este en el mismo directorio. El ADB se resuelve via
`ANDROID_SDK_WIN` o la ruta por defecto de la instalacion Windows SDK.
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# android_emu_battery — Simulate battery state on Android emulator (level + charging).
# Usage: android_emu_battery [--serial <S>] <level 0-100> [--charging <true|false>]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/adb_wsl.sh"
android_emu_battery() {
# Resolve serial (consumes --serial from args, leaves rest in ADB_PICK_REST)
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
# Require serial to be an emulator
if [[ "$serial" != emulator-* ]]; then
echo "android_emu_battery: serial '$serial' is not an emulator (must start with emulator-)." >&2
return 1
fi
# Parse remaining args: positional level + --charging
local level=""
local charging=""
while [[ $# -gt 0 ]]; do
case "$1" in
--charging)
charging="$2"
shift 2
;;
--charging=*)
charging="${1#--charging=}"
shift
;;
-*)
echo "android_emu_battery: unknown flag '$1'." >&2
return 1
;;
*)
if [[ -z "$level" ]]; then
level="$1"
fi
shift
;;
esac
done
# Validate level
if [[ -z "$level" ]]; then
echo "android_emu_battery: level is required (0-100)." >&2
return 1
fi
if ! [[ "$level" =~ ^[0-9]+$ ]] || (( level < 0 || level > 100 )); then
echo "android_emu_battery: invalid level '$level' — must be integer 0-100." >&2
return 1
fi
# Set battery level
adb_s "$serial" emu power capacity "$level" || {
echo "android_emu_battery: failed to set capacity on $serial." >&2
return 1
}
# Set charging state if requested
local ch="<unchanged>"
if [[ -n "$charging" ]]; then
local ac_val
case "$charging" in
true) ac_val="on" ;;
false) ac_val="off" ;;
*)
echo "android_emu_battery: --charging must be 'true' or 'false', got '$charging'." >&2
return 1
;;
esac
adb_s "$serial" emu power ac "$ac_val" || {
echo "android_emu_battery: failed to set AC charging on $serial." >&2
return 1
}
ch="$charging"
fi
echo "battery: ${level}% [charging=${ch}] on ${serial}"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_emu_battery "$@"
fi
@@ -0,0 +1,53 @@
---
name: android_emu_geo_fix
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_emu_geo_fix([--serial <S>], longitude: string, latitude: string, [altitude: string]) -> void"
description: "Fake GPS location on Android emulator via emu geo fix. Emulator-only (not physical devices)."
tags: [android, emulator, geo, gps, location, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional emulator serial. Auto-detected if omitted."
- name: "longitude"
desc: "Longitude (decimal degrees). Passed first — opposite to human lat/lon convention."
- name: "latitude"
desc: "Latitude (decimal degrees)."
- name: "altitude"
desc: "Optional altitude in meters."
output: "Stdout 'GPS set: <lon>, <lat> (alt=...) on <serial>'. Exit 0."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_emu_geo_fix.sh"
---
## Ejemplo
```bash
# Fijar GPS en Madrid (emulador activo)
android_emu_geo_fix -3.7038 40.4168
# Con altitud
android_emu_geo_fix -3.7038 40.4168 650
# Emulador especifico
android_emu_geo_fix --serial emulator-5554 -3.7038 40.4168
```
## Notas
El orden de argumentos es **longitud primero, latitud segundo** — opuesto a la convencion humana habitual (lat/lon). Esto sigue el protocolo del comando `emu geo fix` de Android.
Solo funciona en emuladores (`emulator-*`). Si el serial apunta a un dispositivo fisico, la funcion sale con error y exit 1.
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el dispositivo objetivo.
Sale con exit 3 si no hay ningun device conectado.
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# android_emu_geo_fix — Fake GPS location on Android emulator via emu geo fix.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
android_emu_geo_fix() {
local serial lon lat alt
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
lon="${1:?android_emu_geo_fix: longitude required}"
lat="${2:?android_emu_geo_fix: latitude required}"
alt="${3:-}"
# geo fix only works on emulators, not physical devices
if [[ "$serial" != emulator-* ]]; then
echo "android_emu_geo_fix: geo fix only works on emulators (got '$serial')" >&2
return 1
fi
adb_s "$serial" emu geo fix "$lon" "$lat" ${alt:+"$alt"}
if [[ -n "$alt" ]]; then
echo "GPS set: $lon, $lat (alt=$alt) on $serial"
else
echo "GPS set: $lon, $lat on $serial"
fi
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_emu_geo_fix "$@"
fi
@@ -0,0 +1,49 @@
---
name: android_emu_rotate
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_emu_rotate([--serial <S>] [portrait|landscape|0|90|180|270])"
description: "Rotate emulator screen. Empty=toggle, or fixed orientation. Locks autorotate."
tags: [android, emulator, rotation, orientation, pendiente-usar]
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--serial <S>"
desc: "Optional emulator serial. Picked automatically if only one is connected."
- name: "orientation"
desc: "Empty=toggle via emu rotate, or fixed: portrait/landscape/0/90/180/270."
output: "Stdout 'rotated: <orient> on <serial>'."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_emu_rotate.sh"
---
## Ejemplo
```bash
# Toggle rotation
android_emu_rotate
# Force portrait
android_emu_rotate portrait
# Force landscape on specific emulator
android_emu_rotate --serial emulator-5554 landscape
# Set 270 degrees
android_emu_rotate --serial emulator-5554 270
```
## Notas
Deshabilita autorotate (`accelerometer_rotation 0`) antes de aplicar cualquier orientacion fija, de modo que el sistema no la revierta. El toggle (`emu rotate`) no desactiva autorotate: lo usa directamente el daemon del emulador.
`adb_pick_serial` (de `adb_wsl_bash_infra`) selecciona el unico emulador conectado o falla con exit 3 si hay ambiguedad o ninguno disponible. Los argumentos restantes tras extraer `--serial` quedan en `ADB_PICK_REST`.
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# android_emu_rotate — rotate emulator screen or toggle rotation
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
android_emu_rotate() {
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local arg="${1:-}"
# Disable autorotate before any operation
if [[ -n "$arg" ]]; then
adb_s "$serial" shell settings put system accelerometer_rotation 0
fi
case "$arg" in
"")
# Toggle via emu rotate command
adb_s "$serial" emu rotate
;;
portrait|0)
adb_s "$serial" shell settings put system accelerometer_rotation 0
adb_s "$serial" shell settings put system user_rotation 0
;;
landscape|90)
adb_s "$serial" shell settings put system accelerometer_rotation 0
adb_s "$serial" shell settings put system user_rotation 1
;;
180)
adb_s "$serial" shell settings put system accelerometer_rotation 0
adb_s "$serial" shell settings put system user_rotation 2
;;
270)
adb_s "$serial" shell settings put system accelerometer_rotation 0
adb_s "$serial" shell settings put system user_rotation 3
;;
*)
echo "android_emu_rotate: unknown orientation '$arg'" >&2
echo "Usage: android_emu_rotate [--serial <S>] [portrait|landscape|0|90|180|270]" >&2
return 1
;;
esac
echo "rotated: ${arg:-toggle} on $serial"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_emu_rotate "$@"
fi
@@ -0,0 +1,51 @@
---
name: android_emulator_list
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_emulator_list([--json])"
description: "Lista los AVDs disponibles invocando emulator.exe Windows desde WSL2."
tags: [android, emulator, wsl]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--json"
desc: "Optional flag, outputs JSON array instead of newline-separated names"
output: "Lista de AVDs disponibles en el SDK Windows. Una por linea, o JSON array con --json."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_emulator_list.sh"
notes: "Lee env var EMULATOR o ANDROID_SDK_WIN. Default Windows path: /mnt/c/Users/lucas/AppData/Local/Android/Sdk/emulator/emulator.exe. Exit 0 si lista (incluso vacia). Exit 1 solo si el binario no existe o no es ejecutable."
---
## Ejemplo
```bash
# Listar AVDs (una por linea)
android_emulator_list
# Listar AVDs en formato JSON
android_emulator_list --json
# ["Pixel_7_API_34","Pixel_4_API_30"]
# Sobreescribir ruta del emulador
EMULATOR="/custom/path/emulator.exe" android_emulator_list
# Sobreescribir SDK base
ANDROID_SDK_WIN="/mnt/d/Android/Sdk" android_emulator_list
```
## Notas
El script es ejecutable directamente (`chmod +x`) o invocable con `bash android_emulator_list.sh`.
`emulator.exe -list-avds` imprime warnings a stderr que se descartan con `2>/dev/null`. La captura con `mapfile` filtra ademas lineas vacias para producir una lista limpia.
La variable `EMULATOR` tiene prioridad sobre `ANDROID_SDK_WIN`. Si ninguna esta definida se usa el path Windows por defecto de Lucas.
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# android_emulator_list — Lista los AVDs disponibles invocando emulator.exe Windows desde WSL2.
set -euo pipefail
# Resolve emulator binary
EMULATOR="${EMULATOR:-${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}/emulator/emulator.exe}"
if [[ ! -x "$EMULATOR" ]]; then
echo "error: emulator binary not found or not executable: $EMULATOR" >&2
exit 1
fi
# Parse flags
JSON=false
for arg in "$@"; do
case "$arg" in
--json) JSON=true ;;
*) echo "error: unknown argument: $arg" >&2; exit 1 ;;
esac
done
# Collect AVDs, stripping any warnings emulator.exe prints to stderr
mapfile -t AVDS < <("$EMULATOR" -list-avds 2>/dev/null || true)
if $JSON; then
# Build JSON array
printf '['
first=true
for avd in "${AVDS[@]}"; do
[[ -z "$avd" ]] && continue
if $first; then
printf '"%s"' "$avd"
first=false
else
printf ',"%s"' "$avd"
fi
done
printf ']\n'
else
for avd in "${AVDS[@]}"; do
[[ -z "$avd" ]] && continue
printf '%s\n' "$avd"
done
fi
@@ -0,0 +1,49 @@
---
name: android_emulator_start
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_emulator_start(avd_name: string, timeout_s: int) -> string"
description: "Arranca un AVD en background y espera a que termine de bootear. Idempotente: si ya hay emulador corriendo no lanza otro."
tags: [android, emulator, wsl]
params:
- name: avd_name
desc: "Nombre del AVD a arrancar (visible con android_emulator_list o `emulator.exe -list-avds`)"
- name: timeout_s
desc: "Timeout total en segundos para esperar el boot completo. Opcional, default 180"
output: "Serial del device emulado (ej. emulator-5554) en stdout. Exit 0 = boot completo, exit 1 = timeout o emulador murio."
uses_functions: ["adb_wsl_bash_infra"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_emulator_start.sh"
---
## Ejemplo
```bash
source bash/functions/infra/android_emulator_start.sh
# Arrancar AVD con timeout por defecto (180s)
serial=$(android_emulator_start "Pixel_6_API_34")
echo "Emulador listo: $serial" # emulator-5554
# Con timeout personalizado
serial=$(android_emulator_start "Pixel_6_API_34" 300)
```
## Notas
- Sourcea `adb_wsl.sh` del mismo directorio si existe (provee `ADB`, `adb_run`, `adb_wait_boot`). Si no, usa implementacion inline.
- Resuelve `EMULATOR` y `ADB` desde `ANDROID_SDK_WIN` (default `/mnt/c/Users/lucas/AppData/Local/Android/Sdk`) o desde las variables de entorno `EMULATOR=` / `ADB=` si ya están fijadas.
- Idempotente: si `adb devices` ya muestra un `emulator-*`, imprime "already running" + el serial y sale con exit 0 sin lanzar un segundo proceso.
- Log del emulador en `/tmp/emulator_<avd>.log`. PID en `/tmp/emulator_<avd>.pid`.
- El timeout total se reparte: primera mitad para `adb wait-for-device`, segunda mitad para esperar `sys.boot_completed=1`.
- Diseñado para WSL2 con Android SDK instalado en Windows. En Linux nativo basta cambiar las rutas de los binarios via `EMULATOR=` y `ADB=`.
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# android_emulator_start — Arranca un AVD en background y espera a que bootee.
# Uso: android_emulator_start <avd_name> [timeout_s]
set -euo pipefail
# ---------------------------------------------------------------------------
# Source adb_wsl si está disponible (provee ADB, adb_run, adb_wait_boot)
# ---------------------------------------------------------------------------
_ADB_WSL_SH="$(dirname "${BASH_SOURCE[0]}")/adb_wsl.sh"
if [[ -f "$_ADB_WSL_SH" ]]; then
# shellcheck source=adb_wsl.sh
source "$_ADB_WSL_SH"
else
# Fallback inline: resolver ADB
if [[ -z "${ADB:-}" ]]; then
_sdk_root="${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}"
ADB="${_sdk_root}/platform-tools/adb.exe"
unset _sdk_root
fi
adb_run() { "$ADB" "$@"; }
adb_wait_boot() {
local timeout_s="${1:-120}"
local elapsed=0 interval=3 val
while (( elapsed < timeout_s )); do
val=$(adb_run shell getprop sys.boot_completed 2>/dev/null | tr -d '[:space:]')
[[ "$val" == "1" ]] && return 0
sleep "$interval"
(( elapsed += interval ))
done
echo "android_emulator_start: timeout ${timeout_s}s esperando boot." >&2
return 1
}
fi
# ---------------------------------------------------------------------------
# Resolver EMULATOR
# ---------------------------------------------------------------------------
if [[ -z "${EMULATOR:-}" ]]; then
_sdk_root="${ANDROID_SDK_WIN:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk}"
EMULATOR="${_sdk_root}/emulator/emulator.exe"
unset _sdk_root
fi
# ---------------------------------------------------------------------------
# android_emulator_start <avd_name> [timeout_s]
# ---------------------------------------------------------------------------
android_emulator_start() {
local AVD="${1:?android_emulator_start requiere el nombre del AVD como primer argumento}"
local timeout_s="${2:-180}"
# Validaciones de entorno
if [[ ! -f "$EMULATOR" ]]; then
echo "android_emulator_start: emulator.exe no encontrado en '$EMULATOR'. Fija EMULATOR= o ANDROID_SDK_WIN=." >&2
return 1
fi
if [[ ! -f "$ADB" ]]; then
echo "android_emulator_start: adb.exe no encontrado en '$ADB'. Fija ADB= o ANDROID_SDK_WIN=." >&2
return 1
fi
# Idempotencia: si ya hay un emulador corriendo, salir sin lanzar otro
if adb_run devices 2>/dev/null | grep -q "emulator-"; then
echo "already running"
# Imprimir el serial existente
adb_run devices 2>/dev/null | grep "emulator-" | awk '{print $1}' | head -n1
return 0
fi
local log_file="/tmp/emulator_${AVD}.log"
local pid_file="/tmp/emulator_${AVD}.pid"
# Lanzar emulador en background
"$EMULATOR" -avd "$AVD" -no-boot-anim -no-snapshot-load >"$log_file" 2>&1 &
local emu_pid=$!
echo "$emu_pid" > "$pid_file"
# Esperar a que el dispositivo aparezca en adb
local wait_timeout=$(( timeout_s / 2 ))
if ! timeout "$wait_timeout" adb_run wait-for-device 2>/dev/null; then
echo "android_emulator_start: timeout esperando que el dispositivo aparezca en adb (${wait_timeout}s)." >&2
return 1
fi
# Verificar que el proceso del emulador sigue vivo
if ! kill -0 "$emu_pid" 2>/dev/null; then
echo "android_emulator_start: el proceso del emulador (PID $emu_pid) murió antes de completar el boot." >&2
echo " Log: $log_file" >&2
return 1
fi
# Esperar boot completo (sys.boot_completed=1)
local boot_timeout=$(( timeout_s - wait_timeout ))
if ! adb_wait_boot "$boot_timeout"; then
echo "android_emulator_start: timeout ${timeout_s}s esperando boot completo del AVD '$AVD'." >&2
echo " Log: $log_file" >&2
return 1
fi
# Obtener serial del dispositivo emulado
local serial
serial=$(adb_run devices 2>/dev/null | grep "emulator-" | awk '{print $1}' | head -n1)
echo "$serial"
return 0
}
# Ejecutar si se invoca directamente (no sourceado)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_emulator_start "$@"
fi
@@ -0,0 +1,44 @@
---
name: android_emulator_stop
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_emulator_stop(serial?: string) -> void"
description: "Para uno o todos los emuladores Android via adb emu kill. Si serial esta vacio, detecta todos los emulator-* activos y los para. Idempotente: exit 0 aunque no haya nada que matar."
tags: ["android", "emulator", "wsl", "adb"]
params:
- name: "serial"
desc: "Optional emulator serial (e.g. emulator-5554). Empty = kill all running emulators"
output: "Imprime numero de emuladores parados. Exit 0 idempotente."
uses_functions: ["adb_wsl_bash_infra"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_emulator_stop.sh"
---
## Ejemplo
```bash
# Parar todos los emuladores en ejecucion
android_emulator_stop
# Parar un emulador concreto
android_emulator_stop emulator-5554
# Sobreescribir ruta de adb
ADB=/usr/local/bin/adb android_emulator_stop
```
## Notas
Resuelve `ADB` desde variable de entorno (default: ruta de Android SDK en Windows bajo WSL2).
Usa `adb emu kill` en vez de `adb kill-server` para parar solo el emulador sin afectar al daemon adb.
`set -euo pipefail` activo, pero los fallos de `adb emu kill` se suprimen con `|| true` para mantener idempotencia.
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# android_emulator_stop — Para uno o todos los emuladores Android via adb emu kill.
set -euo pipefail
android_emulator_stop() {
local serial="${1:-}"
local ADB="${ADB:-/mnt/c/Users/lucas/AppData/Local/Android/Sdk/platform-tools/adb.exe}"
local killed=0
if [[ -z "$serial" ]]; then
# Detectar todos los emuladores activos
local serials
serials=$("$ADB" devices 2>/dev/null | grep -E '^emulator-' | awk '{print $1}' || true)
if [[ -z "$serials" ]]; then
echo "android_emulator_stop: no running emulators found"
return 0
fi
while IFS= read -r s; do
[[ -z "$s" ]] && continue
echo "android_emulator_stop: stopping $s"
"$ADB" -s "$s" emu kill 2>/dev/null || true
((killed++)) || true
done <<< "$serials"
else
echo "android_emulator_stop: stopping $serial"
"$ADB" -s "$serial" emu kill 2>/dev/null || true
((killed++)) || true
fi
echo "android_emulator_stop: stopped $killed emulator(s)"
return 0
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_emulator_stop "${1:-}"
fi
@@ -0,0 +1,63 @@
---
name: android_input_keyevent
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_input_keyevent([--serial <S>] key: string)"
description: "Send key event via adb shell input keyevent. Accepts aliases (BACK, HOME, POWER, ENTER, MENU, RECENT_APPS, VOLUME_UP, VOLUME_DOWN), raw numeric codes, or explicit KEYCODE_* names."
tags: [android, adb, input, keyevent, ui-test, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional target device/emulator serial. If omitted, adb_pick_serial resolves the single connected device."
- name: "key"
desc: "Keycode: short alias (BACK/HOME/POWER/ENTER/MENU/RECENT_APPS/VOLUME_UP/VOLUME_DOWN), raw number (e.g. 4, 26), or explicit KEYCODE_* name."
output: "Stdout 'key: <code> on <serial>'."
uses_functions: ["adb_wsl_bash_infra"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_input_keyevent.sh"
notes: "Lista completa de keycodes: https://developer.android.com/reference/android/view/KeyEvent. Exit 3 si adb_pick_serial falla (ningun device o ambiguo sin --serial)."
---
## Ejemplo
```bash
# Pulsar BACK en el unico device conectado
android_input_keyevent BACK
# Pulsar HOME en un emulador especifico
android_input_keyevent --serial emulator-5554 HOME
# Codigo numerico directo
android_input_keyevent 26 # POWER
# KEYCODE_* explicito
android_input_keyevent KEYCODE_DPAD_CENTER
```
## Notas
Aliases resueltos internamente:
| Alias | KEYCODE |
|--------------|-----------------------|
| BACK | KEYCODE_BACK |
| HOME | KEYCODE_HOME |
| POWER | KEYCODE_POWER |
| ENTER | KEYCODE_ENTER |
| MENU | KEYCODE_MENU |
| RECENT_APPS | KEYCODE_APP_SWITCH |
| VOLUME_UP | KEYCODE_VOLUME_UP |
| VOLUME_DOWN | KEYCODE_VOLUME_DOWN |
Si el argumento no coincide con ningun alias y no es numerico, se construye `KEYCODE_<UPPER>` para pasarlo directo a `adb shell input keyevent`.
Exit codes: 1 = keycode vacio, 3 = fallo de `adb_pick_serial` (ningun device o ambiguo).
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# android_input_keyevent — Send key event via adb shell input keyevent.
# Accepts aliases (BACK, HOME, POWER, ENTER, MENU, RECENT_APPS),
# raw numeric codes, or explicit KEYCODE_* names.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
android_input_keyevent() {
# Resolve serial (consumes --serial <S> from args, remainder in ADB_PICK_REST)
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local raw="${1:-}"
if [[ -z "$raw" ]]; then
echo "android_input_keyevent: missing keycode argument" >&2
return 1
fi
# Resolve alias → KEYCODE_*
local keycode
case "${raw^^}" in
BACK) keycode="KEYCODE_BACK" ;;
HOME) keycode="KEYCODE_HOME" ;;
POWER) keycode="KEYCODE_POWER" ;;
ENTER) keycode="KEYCODE_ENTER" ;;
MENU) keycode="KEYCODE_MENU" ;;
RECENT_APPS) keycode="KEYCODE_APP_SWITCH" ;;
VOLUME_UP) keycode="KEYCODE_VOLUME_UP" ;;
VOLUME_DOWN) keycode="KEYCODE_VOLUME_DOWN" ;;
*)
# Already has KEYCODE_ prefix or is a raw number → pass through
if [[ "${raw^^}" == KEYCODE_* ]] || [[ "$raw" =~ ^[0-9]+$ ]]; then
keycode="$raw"
else
# Unknown alias: uppercase and prepend KEYCODE_
keycode="KEYCODE_${raw^^}"
fi
;;
esac
adb_s "$serial" shell input keyevent "$keycode"
echo "key: $keycode on $serial"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_input_keyevent "$@"
fi
@@ -0,0 +1,59 @@
---
name: android_input_swipe
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_input_swipe([--serial <S>], x1: int, y1: int, x2: int, y2: int, [duration_ms: int])"
description: "Send swipe gesture between two points with duration."
tags: [android, adb, input, swipe, gesture, ui-test, pendiente-usar]
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--serial <S>"
desc: "Optional target device serial. Overrides ADB_SERIAL envvar."
- name: x1
desc: "Start X coordinate in pixels."
- name: y1
desc: "Start Y coordinate in pixels."
- name: x2
desc: "End X coordinate in pixels."
- name: y2
desc: "End Y coordinate in pixels."
- name: duration_ms
desc: "Optional swipe duration in milliseconds. Default 300."
output: "Stdout swipe summary line: 'swipe x1,y1 → x2,y2 (Nms) on <serial>'."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_input_swipe.sh"
---
## Ejemplo
```bash
source bash/functions/infra/android_input_swipe.sh
# Scroll down (swipe up)
android_input_swipe 540 1400 540 400
# Scroll up slowly on a specific device
android_input_swipe --serial emulator-5554 540 400 540 1400 800
```
## Notas
Requiere `adb_wsl.sh` (sourceado automáticamente). Usa `adb_pick_serial` para
resolver el dispositivo objetivo a partir de `--serial`, `ADB_SERIAL` o el
único device disponible.
Los cuatro argumentos de coordenadas se validan como enteros antes de invocar
adb — acepta coordenadas negativas (edge cases de hardware con ejes invertidos).
Exit 3 si `adb_pick_serial` no puede resolver el serial (sin devices o ambiguo).
Exit 1 si faltan coordenadas o alguna no es numérica.
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# android_input_swipe — Send swipe gesture between two points via adb shell input swipe.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
# ---------------------------------------------------------------------------
# android_input_swipe [--serial <S>] <x1> <y1> <x2> <y2> [duration_ms]
#
# $1 x1 Start X coordinate in pixels (obligatorio).
# $2 y1 Start Y coordinate in pixels (obligatorio).
# $3 x2 End X coordinate in pixels (obligatorio).
# $4 y2 End Y coordinate in pixels (obligatorio).
# $5 duration_ms Swipe duration in milliseconds (opcional, default 300).
#
# Envvar ADB_SERIAL overrides --serial.
# ---------------------------------------------------------------------------
android_input_swipe() {
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local x1="${1:-}"
local y1="${2:-}"
local x2="${3:-}"
local y2="${4:-}"
local dur="${5:-300}"
if [[ -z "$x1" || -z "$y1" || -z "$x2" || -z "$y2" ]]; then
echo "android_input_swipe: se requieren cuatro argumentos: x1 y1 x2 y2." >&2
return 1
fi
# Validar que los cuatro coordenadas son numericas (enteros o negativos).
local coord
for coord in "$x1" "$y1" "$x2" "$y2"; do
if ! [[ "$coord" =~ ^-?[0-9]+$ ]]; then
echo "android_input_swipe: coordenada no numerica: '$coord'." >&2
return 1
fi
done
adb_s "$serial" shell input swipe "$x1" "$y1" "$x2" "$y2" "$dur"
echo "swipe $x1,$y1$x2,$y2 (${dur}ms) on $serial"
}
# Ejecutar si se llama directamente (no sourceado)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_input_swipe "$@"
fi
+46
View File
@@ -0,0 +1,46 @@
---
name: android_input_tap
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_input_tap([--serial <S>], x: int, y: int) -> void"
description: "Send tap gesture at screen coordinates via adb shell input tap."
tags: [android, adb, input, tap, ui-test, gesture, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional target device serial. Auto-detected if omitted."
- name: "x"
desc: "X coordinate in pixels (non-negative integer)."
- name: "y"
desc: "Y coordinate in pixels (non-negative integer)."
output: "Stdout 'tap @ <x>,<y> on <serial>'."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_input_tap.sh"
---
## Ejemplo
```bash
# Auto-detect device
android_input_tap 540 960
# Target specific device
android_input_tap --serial emulator-5554 540 960
```
## Notas
Sources `adb_wsl.sh` para resolver el binario ADB y exponer `adb_pick_serial` / `adb_s`.
Usa `adb_pick_serial` para consumir `--serial` de los args y autodetectar el device si no se pasa.
Valida X e Y con regex `^[0-9]+$` antes de invocar adb.
Exit 3 si no hay device/emulador disponible (propagado desde `adb_pick_serial`).
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# android_input_tap — Send tap gesture at screen coordinates via adb shell input tap.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
# ---------------------------------------------------------------------------
# android_input_tap [--serial <S>] <x> <y>
#
# --serial <S> Optional target device serial (also auto-detected).
# x X coordinate in pixels (non-negative integer).
# y Y coordinate in pixels (non-negative integer).
#
# Exits:
# 0 tap sent successfully
# 1 missing or invalid coordinates
# 3 no device/emulator available
# ---------------------------------------------------------------------------
android_input_tap() {
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local x="${1:-}"
local y="${2:-}"
if [[ -z "$x" || -z "$y" ]]; then
echo "android_input_tap: se requieren X e Y como argumentos posicionales." >&2
return 1
fi
if [[ ! "$x" =~ ^[0-9]+$ ]]; then
echo "android_input_tap: X debe ser un entero no negativo, recibido '$x'." >&2
return 1
fi
if [[ ! "$y" =~ ^[0-9]+$ ]]; then
echo "android_input_tap: Y debe ser un entero no negativo, recibido '$y'." >&2
return 1
fi
adb_s "$serial" shell input tap "$x" "$y"
echo "tap @ $x,$y on $serial"
}
# Ejecutar si se llama directamente (no sourceado)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_input_tap "$@"
fi
@@ -0,0 +1,52 @@
---
name: android_input_text
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_input_text([--serial <S>], text: string) -> void"
description: "Type text in focused field via adb shell input text. Spaces handled."
tags: [android, adb, input, text, ui-test, pendiente-usar]
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_input_text.sh"
params:
- name: "--serial <S>"
desc: "Optional target device serial. If omitted, autodetects first connected device/emulator."
- name: "text"
desc: "Text to type (spaces become %s as required by adb)."
output: "Stdout 'typed: <text>'. Exit 0."
notes: |
adb input text replaces spaces with %s. Funcion lo hace automaticamente.
Special chars " $ ` se escapan con backslash para evitar interpretacion por el shell.
Exit 3 si no hay ningun device disponible (propagado desde adb_pick_serial).
---
## Ejemplo
```bash
source bash/functions/infra/android_input_text.sh
# Tipar en el device por defecto
android_input_text "hello world"
# → typed: hello world (envia "hello%sworld" a adb)
# Tipar en un device especifico
android_input_text --serial emulator-5554 "user@example.com"
```
## Notas
`adb shell input text` no acepta espacios directos — los convierte a `%s` internamente. Esta funcion hace la sustitucion antes de llamar a adb para que el comportamiento sea predecible.
Los caracteres `"`, `$` y `` ` `` se escapan con backslash para que el shell no los interprete al construir el comando.
Depende de `adb_wsl_bash_infra` para resolver el binario `adb.exe` en WSL2 y para `adb_pick_serial` / `adb_s`.
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# android_input_text — Type text in focused field via adb shell input text.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
# ---------------------------------------------------------------------------
# android_input_text [--serial <S>] <text>
#
# $1 text Text to type in the currently focused field (obligatorio).
# Spaces are replaced with %s as required by adb input text.
# Special chars " $ ` are escaped with backslash.
#
# Envvar ADB_SERIAL overrides --serial.
# ---------------------------------------------------------------------------
android_input_text() {
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local text="${1:-}"
if [[ -z "$text" ]]; then
echo "android_input_text: se requiere el texto como primer argumento." >&2
return 1
fi
# adb input text does not support raw spaces; replace with %s.
# Also escape " $ ` which the shell would interpret inside the adb command.
local escaped
escaped="${text// /%s}"
escaped="${escaped//\"/\\\"}"
escaped="${escaped//\$/\\\$}"
escaped="${escaped//\`/\\\`}"
adb_s "$serial" shell input text "$escaped"
echo "typed: $text"
}
# Ejecutar si se llama directamente (no sourceado)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_input_text "$@"
fi
+59
View File
@@ -0,0 +1,59 @@
---
name: android_logcat
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_logcat([--serial <S>] [--package <name>] [--level <V|D|I|W|E|F>] [--lines <N>] [--clear])"
description: "Lee logcat del device/emulador, opcionalmente filtrado por package y nivel. Multi-emulator via --serial."
tags: [android, adb, logcat, wsl]
uses_functions: ["adb_wsl_bash_infra"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--serial <S>"
desc: "Optional target device/emulator serial. Default: first device detected."
- name: "--package <name>"
desc: "Filter by app package (resolves PID via adb shell pidof)"
- name: "--level <L>"
desc: "Min log level V/D/I/W/E/F, default I"
- name: "--lines <N>"
desc: "Dump last N lines and exit. Default: follow indefinidamente"
- name: "--clear"
desc: "Clear log buffer before reading"
output: "Logcat output a stdout. Follow indefinido sin --lines. Exit 130 si Ctrl-C. Exit 2 si --package y el proceso no corre."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_logcat.sh"
---
## Ejemplo
```bash
# Follow completo sin filtros
android_logcat
# Solo logs de una app, nivel Warning y superior
android_logcat --package com.example.myapp --level W
# Dump de las últimas 200 líneas y salir
android_logcat --lines 200
# Limpiar buffer y hacer follow solo de errores de la app
android_logcat --clear --package com.example.myapp --level E
```
## Notas
- Resuelve `adb` o `adb.exe` en PATH (compatible con WSL2 usando el binario Windows).
- `--package` usa `adb shell pidof -s` para obtener el PID actual. Si la app no está corriendo, sale con exit 2.
- `--lines N` activa modo dump (`-d -t N`); sin él, el follow es indefinido hasta Ctrl-C (exit 130).
- `--clear` ejecuta `adb logcat -c` antes de leer, descartando el buffer acumulado.
- El filtro de nivel se aplica como `*:<level>` al final del comando logcat.
- En follow mode, `trap INT TERM` garantiza exit limpio (exit 130) al interrumpir.
- CR (`\r`) del output de `adb.exe` en WSL se limpia al resolver el PID.
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# android_logcat — Lee logcat del device/emulador, opcionalmente filtrado por package y nivel.
# Multi-emulator via --serial <S>.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "$SCRIPT_DIR/adb_wsl.sh"
android_logcat() {
local serial
adb_pick_serial "$@" || { echo "android_logcat: no device/emulator." >&2; return 3; }
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local package=""
local level="I"
local lines=""
local do_clear=0
while [[ $# -gt 0 ]]; do
case "$1" in
--package) package="$2"; shift 2 ;;
--level) level="$2"; shift 2 ;;
--lines) lines="$2"; shift 2 ;;
--clear) do_clear=1; shift ;;
*) echo "android_logcat: unknown argument: $1" >&2; return 1 ;;
esac
done
if [[ $do_clear -eq 1 ]]; then
adb_s "$serial" logcat -c
fi
local pid_filter=""
if [[ -n "$package" ]]; then
local pid
pid=$(adb_s "$serial" shell pidof -s "$package" 2>/dev/null || true)
pid="${pid//$'\r'/}"
if [[ -z "$pid" ]]; then
echo "android_logcat: package '$package' is not running on $serial" >&2
return 2
fi
pid_filter="--pid=$pid"
fi
local -a cmd=(logcat -v time)
[[ -n "$lines" ]] && cmd+=(-d -t "$lines")
[[ -n "$pid_filter" ]] && cmd+=("$pid_filter")
cmd+=("*:${level}")
trap 'exit 130' INT TERM
adb_s "$serial" "${cmd[@]}"
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
android_logcat "$@"
fi
+46
View File
@@ -0,0 +1,46 @@
---
name: android_pull
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_pull [--serial <S>] remote_path local_path"
description: "Pull file/dir from Android device to WSL via adb pull."
tags: [android, adb, pull, file, transfer, pendiente-usar]
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--serial <S>"
desc: "Optional target device serial. If omitted, adb_pick_serial auto-detects the connected device."
- name: "remote_path"
desc: "Source path on the Android device (e.g. /sdcard/Pictures/foo.png)."
- name: "local_path"
desc: "Destination path in the WSL filesystem. Parent directories are created automatically."
output: "Stdout 'pulled: <remote> → <local> from <serial>'."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_pull.sh"
---
## Ejemplo
```bash
# Pull a single file (auto-detect device)
android_pull /sdcard/Pictures/foo.png ~/Downloads/foo.png
# Pull a directory to a specific local path with explicit serial
android_pull --serial emulator-5554 /sdcard/DCIM ~/Downloads/DCIM
```
## Notas
Sources `adb_wsl.sh` for `adb_pick_serial`, `ADB_PICK_REST`, `adb_wsl_to_win`, and `adb_s`.
The local path is converted to a Windows path via `adb_wsl_to_win` before passing to `adb pull`,
which is required because `adb.exe` (Windows binary) does not understand WSL paths.
Exit code 3 when no device serial can be resolved.
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# android_pull — Pull file/dir from Android device to WSL via adb pull.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/adb_wsl.sh"
android_pull() {
local serial remote local_path win_local
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
remote="${1:?remote_path required}"
local_path="${2:?local_path required}"
mkdir -p "$(dirname "$local_path")"
win_local=$(adb_wsl_to_win "$local_path")
adb_s "$serial" pull "$remote" "$win_local"
echo "pulled: $remote$local_path from $serial"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_pull "$@"
fi
+50
View File
@@ -0,0 +1,50 @@
---
name: android_push
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_push([--serial <S>], local_path: string, remote_path: string) -> void"
description: "Push file/dir from WSL to Android device via adb push."
tags: [android, adb, push, file, transfer, pendiente-usar]
params:
- name: "--serial <S>"
desc: "Optional target device/emulator serial. Auto-detected if omitted."
- name: "local_path"
desc: "WSL source path to file or directory to push."
- name: "remote_path"
desc: "Device destination path, e.g. /sdcard/Download/foo.txt."
output: "Stdout 'pushed: <local> → <remote> on <serial>'. Exit 0."
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_push.sh"
---
## Ejemplo
```bash
# Push a file to the active emulator
android_push /tmp/data.json /sdcard/Download/data.json
# Push to a specific device
android_push --serial emulator-5554 /tmp/data.json /sdcard/Download/data.json
# Push a directory
android_push --serial R5CR1234567 ~/exports/bundle /sdcard/Download/bundle
```
## Notas
Usa `adb_pick_serial` de `adb_wsl.sh` para resolver el dispositivo objetivo.
Si `--serial` no se pasa, autodetecta el primer device/emulador disponible.
Sale con exit 3 si no hay ningun device conectado.
Valida que `local_path` existe en WSL antes de convertir y enviar.
Convierte el path WSL a Windows con `adb_wsl_to_win` (requiere `wslpath`; si no está disponible usa el path tal cual).
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# android_push — Push file/dir from WSL to Android device via adb push.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=adb_wsl.sh
source "$SCRIPT_DIR/adb_wsl.sh"
android_push() {
local serial local_path remote_path win_local
adb_pick_serial "$@" || exit 3
local serial="$ADB_PICK_SERIAL"
set -- "${ADB_PICK_REST[@]}"
local_path="${1:?android_push: local_path required}"
remote_path="${2:?android_push: remote_path required}"
if [[ ! -e "$local_path" ]]; then
echo "android_push: '$local_path' not found." >&2
return 1
fi
win_local=$(adb_wsl_to_win "$local_path")
adb_s "$serial" push "$win_local" "$remote_path"
echo "pushed: $local_path$remote_path on $serial"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
android_push "$@"
fi
@@ -0,0 +1,53 @@
---
name: android_screen_record
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "android_screen_record([--serial <S>] [--duration <s>] [--bit-rate <bps>] [--size <WxH>] output_path: string) -> void"
description: "Record screen video via adb screenrecord, pulls to local path."
tags: [android, adb, screen, record, video, pendiente-usar]
uses_functions: [adb_wsl_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--serial <S>"
desc: "Optional target device serial. If omitted, autodetects first connected device/emulator."
- name: "output_path"
desc: "WSL destination path for the recorded .mp4 file."
- name: "--duration <s>"
desc: "Recording duration in seconds. Default 30, max 180 (adb screenrecord built-in limit)."
- name: "--bit-rate <bps>"
desc: "Video bit rate in bits per second. Default 4000000 (4 Mbps)."
- name: "--size <WxH>"
desc: "Video dimensions e.g. 720x1280. Default: device native resolution."
output: "Stdout 'recorded: <path> (<s>s from <serial>)'. MP4 file written to output_path."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/android_screen_record.sh"
---
## Ejemplo
```bash
source bash/functions/infra/android_screen_record.sh
# Record 15 seconds to a local file
android_screen_record --duration 15 /tmp/demo.mp4
# Specific device, custom resolution, higher bitrate
android_screen_record --serial emulator-5554 --duration 60 --bit-rate 8000000 --size 1080x2400 ~/videos/session.mp4
```
## Notas
`adb screenrecord` tiene un limite maximo de 180 segundos por grabacion. Para capturas mas largas, encadenar multiples llamadas y concatenar los MP4 resultantes (ej. con `ffmpeg -f concat`).
El archivo temporal en el dispositivo es siempre `/sdcard/__rec.mp4` y se elimina tras el pull. Si la grabacion falla a mitad, el archivo puede quedar en el dispositivo; en ese caso ejecutar `adb shell rm /sdcard/__rec.mp4` manualmente.
Exit codes: 0 exito, 2 falta output_path, 3 ningun device encontrado.

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