# Changelog

Todos los cambios notables de `fn_registry` se documentan aquí.

Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/). Al no haber releases semver formales, las entradas se ordenan por fecha.

Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones arquitecturales ver `docs/adr/`.

## [Unreleased]

## 2026-05-17

### Added

- **Bloque `service:` en frontmatter de `app.md`** (issue 0105) — toda app con `tag: service` declara ahora `port`, `health_endpoint`, `health_timeout_s`, `systemd_unit`, `systemd_scope`, `restart_policy`, `runtime` (`systemd-user|systemd-system|docker-compose|stdio|manual`), `pc_targets[]`, `is_local_only`. 11 apps actualizadas: `sqlite_api`, `dag_engine`, `call_monitor`, `kanban`, `deploy_server`, `registry_mcp`, `registry_api`, `footprint_geo_stack`, `element_matrix_chat`, `agents_and_robots`, `services_api`.
- **Migration `014_service_metadata.sql`** — anade 8 columnas (`service_port`, `service_health_endpoint`, `service_health_timeout_s`, `service_systemd_unit`, `service_systemd_scope`, `service_restart_policy`, `service_runtime`, `service_is_local_only`) a `apps` + tabla nueva `service_targets (app_id, pc_id, role)` con indices por `app_id` y `pc_id`.
- **`registry.App.Service *ServiceSpec`** + parser `rawService` + escritura/lectura en `InsertApp`/`scanApps`/`Purge` (preserva `service_targets`). API publica `db.GetServicePCTargets(appID) []string`.
- **`audit_services_spec_go_infra`** (`functions/infra/audit_services_spec.{go,md}`) — audita apps `tag: service` y reporta drift del bloque `service:` (runtime allowlist, pc_targets >=1, systemd_unit obligatorio si `runtime` empieza con `systemd-`, restart_policy en `always|on-failure|none`).
- **`fn doctor services-spec`** — subcomando nuevo en `cmd/fn/doctor.go`. Salida tabwriter + `--json`. Hoy: `11/11 services with complete service: block`.
- **App `services_api`** (`apps/services_api/`, issue 0106) — Go HTTP daemon en `127.0.0.1:8485`. Loop paralelo cada 15s (max 8 in-flight, timeout 20s/probe) que reconcilia esperado vs real para cada `(app, pc)` cruzado de `service_targets`. Probes locales (`systemctl is-active` + TCP dial + `http.Client`) o remotos (`ssh_exec_go_infra`). Persiste en `operations.db`: `service_state` (snapshot actual) + `service_transition` (cambios de overall append-only). Endpoints `GET /api/health`, `GET /api/services`, `POST /api/check`, `GET /api/pcs`. systemd unit `~/.config/systemd/user/services_api.service` con `Restart=always`.
- **App `services_monitor`** (`apps/services_monitor/`, issue 0106) — frontend C++ ImGui. Polling auto cada 5s configurable + boton "Force check" (POST `/api/check`). Tabla 9-col agrupada por app: overall pill, systemd state, port + listening flag (`TI_PLUG`/`TI_PLUG_CONNECTED`), HTTP status+latency, runtime, last change age, error/note. JSON via `vendor/nlohmann/json.hpp` (copiado de data_factory). HTTP socket TCP via `http_client.{cpp,h}` (copiado de data_factory). Build linux + windows con `add_imgui_app` + ws2_32 en Win. Deploy automatico via `redeploy_cpp_app_windows`.
- **Issues 0105 + 0106** (`dev/issues/`) — estandarizacion del bloque `service:` y app `services_monitor`.

### Fixed

- **`sqlite_api.service` murio 20h sin alerta el 2026-05-17** — Raiz: el unit tenia `Restart=on-failure` y el ultimo exit fue por `SIGTERM` (limpio, no failure). systemd NO reinicia exit success. Fix: cambio a `Restart=always` + `RestartSec=5`. Reload + restart inmediato. Detectado mientras se debuggeaba `data_factory` cargando lento (raiz: data_factory llama a `sqlite_api:8484`, timeout 3s, no responde). Aplicado el mismo `Restart=always` al unit nuevo `services_api.service`.
- **`sqlite_api/app.md` health_endpoint** — declaraba `/api/status` que devuelve 404. Cambiado a `/api/databases` (200, lista de bases registradas). Detectado por el primer ciclo del propio `services_api` que marcaba sqlite_api como `degraded`.

### Changed

- **`services_monitor` tags** — sin `service`/`services` en `tags` para evitar falso positivo en el matcher `tags LIKE '%service%'` del audit `services-spec`. La app es desktop client (frontend), no daemon.

## 2026-05-16

### Added

- **Panel "Logs" en `dag_engine` RunDetail** — `apps/dag_engine/frontend/src/pages/RunDetail.tsx` anade `<Paper>` final con `<Code block>` scrollable + `CopyButton` de Mantine. Helper `buildLogText(run, steps)` compone texto plano (metadata del run + por-step status/exit/duration/stdout/stderr indentado) para pegar entero al LLM sin abrir los `Collapse` del `StepTimeline`.

### Fixed

- **`dag_engine` steps `function:` fallando con `error: function "<id>" not found (tried as ID and name)`** — tres DAGs nocturnos (`fn_backup` x2, `daily-registry-audit`) fallaron 2026-05-15/16 porque el binario `fn` resolvia una copia stale `apps/dag_engine/registry.db` (May 15, 262 KB) en vez del `registry.db` raiz. Raiz: el systemd unit `dag_engine.service` tiene `WorkingDirectory=apps/dag_engine/` y no exportaba `FN_REGISTRY_ROOT`; `cmd/fn/ops.go::tryOpenRegistryDB` cae al walk-up `go.mod` (devuelve `apps/dag_engine/`). Fix:
  - Borrado `apps/dag_engine/registry.db` stale (violaba `.claude/rules/db_locations.md`).
  - `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT`, `FN_BIN`, `PATH` (con `/usr/local/go/bin` para steps `function:` Go sin tests que invocan `go vet`), `HOME`.
  - `apps/dag_engine/executor.go`: steps `function:` exportan `FN_REGISTRY_ROOT=<root>` en env y default `dir = fnRegistryRoot` si `step.Dir`/`dag.WorkingDir` vacios. Steps `command:`/`script:` sin cambio.

### Added

- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `<exe_dir>` desplegado.
  - Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop.
  - Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256).
  - Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `<app_dir>/appicon.ico` y genera `<target>_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`).
  - Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`.
  - Convencion documentada en `.claude/rules/cpp_apps.md §11`.

- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame).
- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map<HWND, WNDPROC>`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND).
- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes.
- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal.
- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows.
- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`.

### Changed

- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui).
- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps/<X>/` (canonical issue 0096) ademas de `cpp/apps/<X>/` (legacy) y `projects/*/apps/<X>/`. Fix retroactivo: `./fn run compile_cpp_app <name>` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`.

### Notes

- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos.
- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`).
- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion.

## 2026-05-14

### Added

- **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

### Added

- 6 funciones `bash/infra/systemd_local_*` (install_unit, enable, start, restart, status, uninstall) para gestionar servicios systemd del sistema desde el registry (complementa las versiones remotas SSH ya existentes).
- Pipeline `install_systemd_service_bash_pipelines` que compone las anteriores: genera unit file + install + enable + start + status.
- Servicio systemd `sqlite_api.service` instalado y habilitado en aurgi-pc — arranque automático al iniciar WSL en `127.0.0.1:8484`.
- `projects/fn_monitoring/launcher.sh` — launcher del dashboard (arranca API si no está + lanza ventana + cleanup).
- Regla [`.claude/rules/kiss.md`](.claude/rules/kiss.md) — filosofía KISS para proyectos y apps.
- Documentación ADR en `docs/adr/` con plantilla y ADR 0001 (experimento GitButler).
- Diario en `docs/diary/` + slash command `/entrada_diario` para añadir entradas.
- `CHANGELOG.md` (este archivo).
- Submódulo `cpp/vendor/glfw` re-registrado con path limpio (antes heredado con path absoluto `/home/lucas/...`).
- aurgi-pc registrado en el server centralizado (`registry.organic-machine.com`) con 18 pc_locations.

### Changed

- `registry.db` ahora está gitignorada. Es regenerable con `fn index` + completable con `fn sync`. Evita conflictos entre ramas y PCs.
- `sqlite_api` ahora se distribuye como binario compilado (`projects/fn_monitoring/apps/sqlite_api/sqlite_api`) en lugar de `go run` al vuelo.

### Fixed

- `http_client.cpp` del dashboard: añadido `#include <cstdint>` requerido por mingw-w64 para cross-compile Windows (g++ Linux lo incluía transitivamente).
- `registry_dashboard.exe` (Windows) ya no abre ventana de consola al lanzarse — enlazado como GUI app (`WIN32_EXECUTABLE TRUE` / `-mwindows`).

### Added (design system C++)

- `cpp/functions/core/tokens` — design tokens para dashboards ImGui (colors, spacing, radius, font_size) inspirados en `@fn_library` (Mantine v9). Paleta dark + indigo primary. `apply_dark_theme()` aplica los tokens al `ImGuiStyle` global.
- `cpp/functions/core/badge` — etiqueta inline con 6 variantes (Default/Success/Warning/Error/Info/Outline). Equivalente a `<Badge>` de `@fn_library`.
- `cpp/functions/core/empty_state` — placeholder centrado para tablas/listas vacías.
- `cpp/functions/core/page_header` — header de página con título/subtítulo + hueco para acciones + separator.
- `registry_dashboard` migrado a los nuevos componentes: `page_header_begin/end` en el header, `empty_state` en las 4 tablas cuando están vacías, `apply_dark_theme()` al primer frame. Sin hardcode de colores disperso.
- `systemd_local_{enable,start,restart}`: stdout de `systemctl` redirigido a stderr para no contaminar el JSON capturado por el pipeline.
- `.gitmodules`: entry fantasma `cpp/vendor/glfw` con path absoluto `/home/lucas/...` que bloqueaba `git submodule status` y el cross-compile Windows.

### Removed

- Integración de GitButler de Claude Code — binario `~/.local/bin/but`, plugin `gitbutler-tools`, skill `.claude/skills/gitbutler/`, hooks en `settings.json`, ramas `gitbutler/*` + `e-branch-*`, estado interno `.git/gitbutler/`. Ver [ADR 0001](docs/adr/0001-gitbutler-experiment.md) para motivos.
