# functions

id: pass_sync_bash_infra
name: pass_sync
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_sync() -> json
description: Sincroniza el password store con el repositorio git remoto (pull + push).
tags: ["pass","secret","sync","git"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["sincroniza con remoto"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_sync.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c1a5fdca7d75b68514f4460c1a2eb61137465659facb4db1dcd14969e94bf793
params_schema: {"output":"JSON con resultados de pull y push"}
example:
  ```bash
  source pass_sync.sh
  result=$(pass_sync)
  # {"pull":"Already up to date.","push":"Everything up-to-date"}
  ```

notes:
  Ejecuta `pass git pull` seguido de `pass git push`. Requiere que el password store tenga un remote git configurado. Retorna JSON con la ultima linea de cada operacion.

code:
  # pass_sync
  # ---------
  # Sincroniza el password store con el repositorio git remoto (pull + push).
  # Sale con exit code 1 si la sincronizacion falla.
  #
  # USO (sourced):
  #   source pass_sync.sh
  #   pass_sync

  pass_sync() {
      local pull_out
      pull_out=$(pass git pull 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_sync: fallo en git pull: $pull_out" >&2
          return 1
      fi

      local push_out
      push_out=$(pass git push 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_sync: fallo en git push: $push_out" >&2
          return 1
      fi

      printf '{"pull":"%s","push":"%s"}' \
          "$(echo "$pull_out" | tail -1 | sed 's/"/\\"/g')" \
          "$(echo "$push_out" | tail -1 | sed 's/"/\\"/g')"
  }


id: rsync_deploy_bash_infra
name: rsync_deploy
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: rsync_deploy(local_dir: string, ssh_alias: string, remote_dir: string) -> json
tags: ["rsync","deploy","sync","ssh","remote","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/rsync_deploy.sh
created_at: 2026-04-12T12:48:21Z
updated_at: 2026-04-12T12:48:21Z
props: []
emits: []
variant: []
content_hash: 31262b8381016bf94bc338c93dca76d1359b0f364519aba3b32dd8b2fe109404
description:
  Sincroniza un directorio local a un host remoto via rsync+SSH. Excluye archivos de desarrollo y bases de datos locales. Crea el directorio remoto si no existe.

example:
  ```bash
  source bash/functions/infra/rsync_deploy.sh

  # Deploy de una app al servidor de producción
  result=$(rsync_deploy "apps/dag_engine/" "prod-server" "/opt/apps/dag_engine")
  echo "$result"
  # {"files_transferred": 12, "total_size": "1.23 MB", "ssh_alias": "prod-server", "remote_dir": "/opt/apps/dag_engine"}

  # Deploy con ruta absoluta local
  rsync_deploy "/home/lucas/fn_registry/apps/myapp/" "myserver" "/opt/myapp"
  ```

notes:
  - Usa `rsync -avz --delete`: archivos borrados localmente se borran también en el remoto.
  - Antes del rsync crea el directorio remoto con `ssh mkdir -p` para evitar errores si no existe.
  - Archivos excluidos: `.git`, `operations.db*`, `*.exe`, `node_modules`, `.venv`, `__pycache__`, `build/`, `*.db-shm`, `*.db-wal`.
  - El JSON de salida va a stdout; los mensajes de progreso y errores van a stderr.
  - Exit code 1 si rsync falla o si el directorio local no existe.
  - El `ssh_alias` se resuelve con la configuración de `~/.ssh/config`, incluyendo host, user, identityfile y puerto.

code:
  #!/usr/bin/env bash
  # rsync_deploy — Sincroniza un directorio local a un host remoto via rsync+SSH
  set -euo pipefail

  rsync_deploy() {
      local local_dir="$1"
      local ssh_alias="$2"
      local remote_dir="$3"

      if [[ -z "$local_dir" || -z "$ssh_alias" || -z "$remote_dir" ]]; then
          echo "rsync_deploy: se requieren local_dir, ssh_alias y remote_dir" >&2
          return 1
      fi

      if [[ ! -d "$local_dir" ]]; then
          echo "rsync_deploy: directorio local '$local_dir' no existe" >&2
          return 1
      fi

      # Crear directorio remoto si no existe
      echo "rsync_deploy: verificando directorio remoto '$remote_dir' en '$ssh_alias'..." >&2
      if ! ssh "$ssh_alias" "mkdir -p '$remote_dir'" 2>&1; then
          echo "rsync_deploy: no se pudo crear el directorio remoto '$remote_dir' en '$ssh_alias'" >&2
          return 1
      fi

      # Ejecutar rsync y capturar salida para parsear estadísticas
      local rsync_output
      rsync_output=$(rsync -avz --delete \
          --exclude='.git' \
          --exclude='operations.db*' \
          --exclude='*.exe' \
          --exclude='node_modules' \
          --exclude='.venv' \
          --exclude='__pycache__' \
          --exclude='build/' \
          --exclude='*.db-shm' \
          --exclude='*.db-wal' \
          -e ssh \
          "$local_dir" \
          "${ssh_alias}:${remote_dir}" 2>&1) || {
          echo "rsync_deploy: rsync falló al sincronizar '$local_dir' → '${ssh_alias}:${remote_dir}'" >&2
          echo "$rsync_output" >&2
          return 1
      }

      echo "$rsync_output" >&2

      # Parsear número de archivos transferidos
      local files_transferred
      files_transferred=$(echo "$rsync_output" | grep -oP 'Number of regular files transferred: \K[0-9,]+' | tr -d ',' || echo "0")
      if [[ -z "$files_transferred" ]]; then
          # Intentar formato alternativo de rsync
          files_transferred=$(echo "$rsync_output" | grep -oP 'Number of files transferred: \K[0-9,]+' | tr -d ',' || echo "0")
      fi
      if [[ -z "$files_transferred" ]]; then
          files_transferred="0"
      fi

      # Parsear tamaño total transferido
      local total_size
      total_size=$(echo "$rsync_output" | grep -oP 'Total transferred file size: \K[0-9,.]+ \w+' || echo "0 bytes")
      if [[ -z "$total_size" ]]; then
          total_size="0 bytes"
      fi

      # Emitir JSON a stdout
      printf '{"files_transferred": %s, "total_size": "%s", "ssh_alias": "%s", "remote_dir": "%s"}\n' \
          "$files_transferred" \
          "$total_size" \
          "$ssh_alias" \
          "$remote_dir"
  }

  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
      rsync_deploy "$@"
  fi

params_schema:
  {"params":[{"name":"local_dir","desc":"ruta al directorio local a sincronizar (ej: apps/dag_engine/)"},{"name":"ssh_alias","desc":"alias SSH del host destino definido en ~/.ssh/config (ej: myserver)"},{"name":"remote_dir","desc":"ruta absoluta del directorio destino en el host remoto (ej: /opt/apps/dag_engine)"}],"output":"JSON con files_transferred (int), total_size (string), ssh_alias (string), remote_dir (string)"}


id: uv_add_packages_bash_infra
name: uv_add_packages
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: uv_add_packages(project_dir: string, ...packages: string) -> void
tags: ["python","uv","pip","packages","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/uv_add_packages.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f807adf1f48d52b92091c7c273f766d781e94ee95bb84908634f583aa688547c
description:
  Instala paquetes Python en un proyecto usando uv add con fallback a pip. Inicializa pyproject.toml si no existe.

example:
  ```bash
  source uv_add_packages.sh
  uv_add_packages /home/lucas/analysis/finanzas jupyter jupyterlab pandas numpy

  # Solo un paquete
  uv_add_packages . polars
  ```

notes:
  Requiere que el venv ya exista (usa `init_uv_venv` antes). Prefiere uv por velocidad y reproducibilidad (lockfile). Si uv no esta disponible, usa pip del venv directamente.

code:
  # uv_add_packages
  # -----------------
  # Instala paquetes Python en un proyecto con uv add.
  # Inicializa pyproject.toml si no existe.
  # Fallback a pip install si uv no esta disponible.
  #
  # USO (sourced):
  #   source uv_add_packages.sh
  #   uv_add_packages /path/to/project jupyter jupyterlab pandas

  uv_add_packages() {
      local project_dir="$1"
      shift
      local packages=("$@")

      if [ ${#packages[@]} -eq 0 ]; then
          echo "uv_add_packages: se requiere al menos un paquete" >&2
          return 1
      fi

      if [ ! -d "$project_dir/.venv" ]; then
          echo "uv_add_packages: no existe .venv en $project_dir — ejecuta init_uv_venv primero" >&2
          return 1
      fi

      if command -v uv &>/dev/null; then
          # Inicializar pyproject.toml si no existe
          if [ ! -f "$project_dir/pyproject.toml" ]; then
              (cd "$project_dir" && uv init 2>/dev/null) || true
          fi
          (cd "$project_dir" && uv add "${packages[@]}" 2>&1)
      else
          "$project_dir/.venv/bin/pip" install "${packages[@]}" 2>&1
      fi
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto con venv existente"},{"name":"packages","desc":"nombres de paquetes Python a instalar (variadic)"}],"output":"sin salida"}


id: write_claude_jupyter_rules_bash_infra
name: write_claude_jupyter_rules
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_claude_jupyter_rules([project_dir: string]) -> string
tags: ["claude","jupyter","rules","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_claude_jupyter_rules.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 66e06d51635c07b708c2b274aeff6961ce58197db2ac3c66fa3816a0bbdb1030
description:
  Genera o actualiza .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter: celdas inmutables, programacion funcional, uso de MCP, acceso al fn_registry.

example:
  ```bash
  source write_claude_jupyter_rules.sh
  path=$(write_claude_jupyter_rules /home/lucas/analysis/finanzas)
  echo "Reglas escritas en: $path"
  ```

notes:
  Idempotente: si CLAUDE.md ya contiene las reglas (detecta "JUPYTER HABILITADO"), no las duplica. Si CLAUDE.md existe sin las reglas, las prepend al contenido existente. Incluye instrucciones de acceso al fn_registry via `FN_REGISTRY_ROOT`.

code:
  # write_claude_jupyter_rules
  # ----------------------------
  # Genera .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter.
  # Si ya existe CLAUDE.md y no tiene las reglas, las prepend.
  #
  # USO (sourced):
  #   source write_claude_jupyter_rules.sh
  #   write_claude_jupyter_rules /path/to/project

  write_claude_jupyter_rules() {
      local project_dir="${1:-.}"
      local claude_dir="${project_dir}/.claude"
      local claude_md="${claude_dir}/CLAUDE.md"

      mkdir -p "$claude_dir"

      # Si ya tiene las reglas, no hacer nada
      if [ -f "$claude_md" ] && grep -q "JUPYTER HABILITADO" "$claude_md" 2>/dev/null; then
          echo "$claude_md"
          return 0
      fi

      local rules
      rules='# JUPYTER HABILITADO EN ESTE ANALISIS

  ## Reglas OBLIGATORIAS para Claude

  ### 1. CODIGO INMUTABLE — NUNCA MODIFICAR CELDAS EXISTENTES
  - **PROHIBIDO** usar NotebookEdit para reemplazar celdas existentes
  - **SIEMPRE** anadir celdas NUEVAS al final del notebook
  - Si hay un error en una celda, crear celda nueva con la correccion
  - El historial de trabajo debe quedar intacto para trazabilidad

  ### 2. PROGRAMACION FUNCIONAL OBLIGATORIA
  - **Funciones puras**: sin efectos secundarios, mismo input -> mismo output
  - **Inmutabilidad**: nunca mutar datos, crear copias transformadas
  - **Composicion**: funciones pequenas que se combinan
  - Preferir: `map`, `filter`, `reduce`, list comprehensions
  - Evitar: loops con mutacion, `global`, modificar argumentos in-place

  ### 3. SIEMPRE usar MCP jupyter para ejecutar codigo Python
  - Las ejecuciones se ven en tiempo real en Jupyter Lab del usuario
  - Compartimos variables y estado del kernel
  - **NUNCA usar bash para ejecutar Python en este analisis**

  ### 4. Verificar Jupyter activo ANTES de ejecutar
  - Si no esta activo: pedir al usuario que ejecute `./run-jupyter-lab.sh`

  ### 5. Gestion de notebooks
  - Notebooks en la carpeta `notebooks/` o subcarpetas
  - Si un notebook tiene >50 celdas, crear uno nuevo
  - Nombrar descriptivamente: `01_exploracion.ipynb`, `02_limpieza.ipynb`

  ### 6. Gestion de Python
  - **SIEMPRE usar `uv`** para gestionar dependencias
  - Anadir paquetes con `uv add nombre_paquete`

  ### 7. Acceso al fn_registry
  - `FN_REGISTRY_ROOT` apunta a la raiz del registry
  - Para importar funciones Python: `sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))`
  - Para consultar registry.db: `sqlite3` o `import sqlite3` con la ruta `$FN_REGISTRY_ROOT/registry.db`

  '

      if [ -f "$claude_md" ]; then
          local existing
          existing=$(cat "$claude_md")
          printf '%s\n%s' "$rules" "$existing" > "$claude_md"
      else
          echo "$rules" > "$claude_md"
      fi

      echo "$claude_md"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del archivo .claude/CLAUDE.md generado o actualizado"}


id: write_jupyter_launcher_bash_infra
name: write_jupyter_launcher
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_jupyter_launcher([project_dir: string]) -> string
tags: ["jupyter","launcher","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_jupyter_launcher.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c59337b64b64deec591f79895e9f25008bf166a7bd38613bf7671f37999eff01
description:
  Genera un script run-jupyter-lab.sh que lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto y token deshabilitado.

example:
  ```bash
  source write_jupyter_launcher.sh
  path=$(write_jupyter_launcher /home/lucas/analysis/finanzas)
  echo "Launcher creado en: $path"

  # Luego en otra terminal:
  # ./run-jupyter-lab.sh [puerto]
  ```

notes:
  El launcher generado:
  - Autodetecta un puerto libre (8888-8899)
  - Guarda el puerto en `.jupyter-port` para que otros procesos lo lean
  - Activa el venv automaticamente
  - Lanza Jupyter Lab en modo `--collaborative` (requiere jupyter-collaboration)
  - Token y password deshabilitados para acceso local

code:
  # write_jupyter_launcher
  # -----------------------
  # Genera un script run-jupyter-lab.sh en el directorio dado.
  # El script lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto.
  #
  # USO (sourced):
  #   source write_jupyter_launcher.sh
  #   write_jupyter_launcher /path/to/project

  write_jupyter_launcher() {
      local project_dir="${1:-.}"
      local launcher="${project_dir}/run-jupyter-lab.sh"

      cat > "$launcher" << 'LAUNCHER'
  #!/bin/bash
  # Jupyter Lab — modo colaborativo con autodeteccion de puerto
  # Generado por write_jupyter_launcher (fn_registry)

  find_free_port() {
      for port in 8888 8889 8890 8891 8892 8893 8894 8895 8896 8897 8898 8899; do
          if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
             ! lsof -i:"$port" >/dev/null 2>&1; then
              echo $port
              return
          fi
      done
      echo 8888
  }

  PORT=${1:-$(find_free_port)}
  cd "$(dirname "$0")"

  echo $PORT > .jupyter-port

  source .venv/bin/activate 2>/dev/null || true

  if ! python -c "import jupyter_collaboration" 2>/dev/null; then
      echo "ERROR: jupyter-collaboration no esta instalado"
      echo "Instala con: uv add jupyter-collaboration"
      exit 1
  fi

  echo "════════════════════════════════════════════════"
  echo "  Jupyter Lab + Colaboracion en puerto $PORT"
  echo "════════════════════════════════════════════════"
  echo ""
  echo "  Abre: http://localhost:$PORT"
  echo "  Ctrl+C para detener"
  echo ""

  jupyter lab \
      --port=$PORT \
      --no-browser \
      --ServerApp.token='' \
      --ServerApp.password='' \
      --ServerApp.disable_check_xsrf=True \
      --ServerApp.allow_origin='*' \
      --ServerApp.root_dir="$(pwd)" \
      --collaborative
  LAUNCHER

      chmod +x "$launcher"
      echo "$launcher"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del script run-jupyter-lab.sh generado"}


id: write_jupyter_registry_kernel_bash_infra
name: write_jupyter_registry_kernel
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_jupyter_registry_kernel([project_dir: string]) -> string
tags: ["jupyter","ipython","kernel","registry","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_jupyter_registry_kernel.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 0ee25c11974d164c5ceb94314112029d7636ab8210b9cfa735dd1688caa5da38
description:
  Genera un script de startup de IPython que autoconfigura FN_REGISTRY_ROOT, sys.path a python/functions del registry, y helpers fn_query/fn_search/fn_code para consultar registry.db desde notebooks.

example:
  ```bash
  source write_jupyter_registry_kernel.sh
  path=$(write_jupyter_registry_kernel /home/lucas/analysis/finanzas)
  echo "Startup creado en: $path"
  ```

  Luego en cualquier notebook del proyecto:

  ```python
  # Ya disponible automaticamente al abrir el notebook:

  # Buscar funciones
  fn_search("finance")

  # Consultar registry.db directamente
  fn_query("SELECT id, signature FROM functions WHERE domain = ?", ("core",))

  # Ver codigo de una funcion
  print(fn_code("filter_list_py_core"))

  # Importar funciones Python del registry directamente
  from core import filter_list, map_list
  from finance import sma, ema, rsi
  ```

notes:
  Genera `.ipython/profile_default/startup/00_fn_registry.py` que se ejecuta automaticamente al iniciar cualquier kernel IPython en el proyecto. No requiere imports manuales — las funciones `fn_query`, `fn_search` y `fn_code` estan disponibles inmediatamente en cada notebook.

  El `sys.path` se configura para que cada dominio de `python/functions/` sea importable directamente (`from core import filter_list`).

  Detecta `FN_REGISTRY_ROOT` buscando la raiz del repo git o subiendo directorios hasta encontrar `registry.db`.

code:
  # write_jupyter_registry_kernel
  # -------------------------------
  # Genera un script de startup de IPython que autoconfigura el acceso
  # al fn_registry en cada notebook: FN_REGISTRY_ROOT, sys.path a
  # python/functions, y un helper fn_query() para consultar registry.db.
  #
  # USO (sourced):
  #   source write_jupyter_registry_kernel.sh
  #   write_jupyter_registry_kernel /path/to/project

  write_jupyter_registry_kernel() {
      local project_dir="${1:-.}"
      local startup_dir="${project_dir}/.ipython/profile_default/startup"
      local registry_root
      registry_root="$(cd "$project_dir" && cd "$(git -C "$project_dir" rev-parse --show-toplevel 2>/dev/null || echo "../..")" && pwd)"

      # Fallback: si no es git, buscar registry.db subiendo directorios
      if [ ! -f "$registry_root/registry.db" ] && [ -f "$project_dir/../../registry.db" ]; then
          registry_root="$(cd "$project_dir/../.." && pwd)"
      fi

      mkdir -p "$startup_dir"

      cat > "${startup_dir}/00_fn_registry.py" << STARTUP
  """
  fn_registry kernel startup
  Autoconfigura acceso al registry en cada notebook.
  Generado por write_jupyter_registry_kernel (fn_registry).
  """
  import os
  import sys
  import sqlite3
  from pathlib import Path

  # ── FN_REGISTRY_ROOT ────────────────────────────────────────
  FN_REGISTRY_ROOT = Path("${registry_root}")
  os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)

  # ── sys.path: importar funciones Python del registry ────────
  _python_functions = FN_REGISTRY_ROOT / "python" / "functions"
  for _domain in sorted(_python_functions.iterdir()) if _python_functions.exists() else []:
      if _domain.is_dir() and not _domain.name.startswith("_"):
          _path = str(_domain)
          if _path not in sys.path:
              sys.path.insert(0, _path)

  # Tambien el directorio padre para imports por dominio: from core import filter_list
  _pf = str(_python_functions)
  if _pf not in sys.path:
      sys.path.insert(0, _pf)

  # ── fn_query: consultar registry.db desde el notebook ───────
  _REGISTRY_DB = FN_REGISTRY_ROOT / "registry.db"

  def fn_query(sql, params=()):
      """Ejecuta una consulta SQL sobre registry.db y retorna las filas.

      Ejemplos:
          fn_query("SELECT id, description FROM functions WHERE domain = ?", ("finance",))
          fn_query("SELECT id FROM functions_fts WHERE functions_fts MATCH ?", ("slice*",))
      """
      if not _REGISTRY_DB.exists():
          raise FileNotFoundError(f"registry.db no encontrado en {_REGISTRY_DB}")
      con = sqlite3.connect(str(_REGISTRY_DB))
      con.row_factory = sqlite3.Row
      try:
          rows = con.execute(sql, params).fetchall()
          return [dict(r) for r in rows]
      finally:
          con.close()

  def fn_search(term):
      """Busca funciones y tipos en el registry por nombre o descripcion.

      Ejemplo:
          fn_search("slice")
          fn_search("finance")
      """
      fts_term = f"name:{term}* OR description:{term}*"
      functions = fn_query(
          "SELECT id, kind, purity, lang, description FROM functions "
          "WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?) "
          "ORDER BY name", (fts_term,)
      )
      types = fn_query(
          "SELECT id, algebraic, lang, description FROM types "
          "WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?) "
          "ORDER BY name", (fts_term,)
      )
      return {"functions": functions, "types": types}

  def fn_code(function_id):
      """Retorna el codigo fuente de una funcion del registry.

      Ejemplo:
          print(fn_code("filter_list_py_core"))
      """
      rows = fn_query("SELECT code FROM functions WHERE id = ?", (function_id,))
      if not rows:
          raise KeyError(f"Funcion no encontrada: {function_id}")
      return rows[0]["code"]

  # ── Mensaje de bienvenida ───────────────────────────────────
  print(f"fn_registry conectado: {FN_REGISTRY_ROOT}")
  print(f"  registry.db: {'OK' if _REGISTRY_DB.exists() else 'NO ENCONTRADO'}")
  print(f"  Python functions: {_pf}")
  print(f"  Helpers: fn_query(), fn_search(), fn_code()")
  STARTUP

      echo "${startup_dir}/00_fn_registry.py"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del archivo startup generado"}


id: write_mcp_jupyter_config_bash_infra
name: write_mcp_jupyter_config
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_mcp_jupyter_config([project_dir: string], [port: int]) -> string
tags: ["mcp","jupyter","config","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_mcp_jupyter_config.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ec2747315cff0aa1f460e7b84e6c6a0d48c67005534f729586341256be0ab91e
description:
  Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server apuntando al venv local y puerto dado. Merge con jq si ya existe.

example:
  ```bash
  source write_mcp_jupyter_config.sh
  path=$(write_mcp_jupyter_config /home/lucas/analysis/finanzas 8890)
  echo "Config MCP en: $path"
  ```

notes:
  El MCP se invoca como modulo Python (`python -m jupyter_mcp_server`) usando el python del venv local, nunca una instalacion global. Si `.mcp.json` ya existe y jq esta disponible, hace merge conservando otros servidores MCP. Sin jq, sobrescribe el archivo.

code:
  # write_mcp_jupyter_config
  # -------------------------
  # Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server.
  # Usa el python del venv local con -m jupyter_mcp_server.server.
  # Configura via env vars (SERVER_URL, TOKEN) — no CLI args.
  # Hace merge si ya existe .mcp.json (requiere jq).
  #
  # USO (sourced):
  #   source write_mcp_jupyter_config.sh
  #   write_mcp_jupyter_config /path/to/project 8888

  write_mcp_jupyter_config() {
      local project_dir="${1:-.}"
      local port="${2:-8888}"
      local mcp_file="${project_dir}/.mcp.json"
      local abs_project
      abs_project="$(cd "$project_dir" && pwd)"

      local python_bin="${abs_project}/.venv/bin/python"
      if [ ! -f "$python_bin" ]; then
          echo "write_mcp_jupyter_config: python no encontrado en ${python_bin}" >&2
          return 1
      fi

      # Verificar que el modulo esta instalado
      if ! "$python_bin" -c "import jupyter_mcp_server" 2>/dev/null; then
          echo "write_mcp_jupyter_config: jupyter_mcp_server no instalado en el venv" >&2
          return 1
      fi

      local new_config
      new_config=$(cat << EOF
  {
    "mcpServers": {
      "jupyter": {
        "command": "${python_bin}",
        "args": ["-m", "jupyter_mcp_server.server"],
        "env": {
          "SERVER_URL": "http://localhost:${port}",
          "TOKEN": ""
        }
      }
    }
  }
  EOF
  )

      if [ -f "$mcp_file" ] && command -v jq &>/dev/null; then
          # Merge conservando otros servidores MCP
          jq -s '.[0] * {mcpServers: ((.[0].mcpServers // {}) * (.[1].mcpServers // {}))}' \
              "$mcp_file" <(echo "$new_config") > "${mcp_file}.tmp"
          mv "${mcp_file}.tmp" "$mcp_file"
      else
          echo "$new_config" > "$mcp_file"
      fi

      echo "$mcp_file"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"},{"name":"port","desc":"puerto Jupyter (default: detectado automáticamente)"}],"output":"ruta del archivo .mcp.json generado o actualizado"}


id: audit_registry_paths_bash_pipelines
name: audit_registry_paths
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: audit_registry_paths([output_file: string]) -> void
tags: ["registry","audit","validation","paths","launcher","pipeline","bash"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/audit_registry_paths.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 6b6fc9847fe3e4a13cb613cb0e725abce527c710b7c2c4e444e43a70c3aee39c
description:
  Audita file_path de todas las functions y types en registry.db, verifica que cada ruta apunte a un archivo existente en disco, y genera un txt con las rutas rotas para que agentes puedan corregirlas.

uses_functions:
  ["assert_command_exists_bash_shell","assert_file_exists_bash_shell","validate_registry_paths_bash_shell","report_execution_json_bash_shell","exit_with_status_bash_shell"]

example:
  ```bash
  # Con default (genera broken_paths.txt en la raiz)
  ./bash/functions/pipelines/audit_registry_paths.sh

  # Con ruta personalizada
  ./bash/functions/pipelines/audit_registry_paths.sh /tmp/broken.txt

  # Desde fn run (pipeline launcher)
  fn run audit_registry_paths
  ```

notes:
  El archivo de salida es consumible por agentes: cada linea tiene el ID de la funcion/tipo y el file_path que necesita correccion. El agente puede leer el .md correspondiente, encontrar el archivo real en disco, y actualizar el campo `file_path`.

documentation:
  ## Flujo

  1. `assert_command_exists` — verifica que `sqlite3` esta disponible
  2. `assert_file_exists` — verifica que `registry.db` existe y reporta su tamano
  3. `validate_registry_paths` (functions) — itera todas las functions, verifica cada file_path
  4. `validate_registry_paths` (types) — itera todos los types, verifica cada file_path
  5. Genera `broken_paths.txt` con formato legible para agentes
  6. `report_execution_json` — imprime JSON de ejecucion a stdout
  7. `exit_with_status` — exit code segun resultado

  ## Formato de salida

  ```
  # Broken file_path entries in registry.db
  # Generated: 2026-04-03T10:00:00Z
  # Total: 11 broken paths
  #
  # Format: id | file_path (in .md) | domain | table
  # ---

  ## Functions (11)

  cdp_click_go_browser | functions/infra/cdp_click.go | browser | functions
  ```

code:
  #!/usr/bin/env bash
  # audit_registry_paths
  # --------------------
  # Audita file_path de functions y types en registry.db.
  # Genera un txt con las rutas rotas para que agentes puedan arreglarlas.
  #
  # Compone: assert_command_exists + assert_file_exists +
  #          validate_registry_paths + report_execution_json + exit_with_status
  #
  # USO:
  #   ./audit_registry_paths.sh [OUTPUT_FILE]
  #
  # ARGUMENTOS (opcionales):
  #   OUTPUT_FILE   Ruta del archivo de salida
  #                 Default: $REGISTRY_ROOT/broken_paths.txt

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/validate_registry_paths.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/report_execution_json.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/exit_with_status.sh"

  OUTPUT_FILE="${1:-$REGISTRY_ROOT/broken_paths.txt}"
  DB_PATH="$REGISTRY_ROOT/registry.db"

  STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  START_MS=$(date +%s%3N)
  STEPS_FILE=$(mktemp)
  trap 'rm -f "$STEPS_FILE"' EXIT

  ok_steps=0
  failed_steps=0

  log_step() {
      local name="$1" action="$2" status="$3" elapsed="$4" output="${5:-}" error="${6:-}"
      printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$action" "$status" "$elapsed" "$output" "$error" >> "$STEPS_FILE"
      if [[ "$status" == "ok" ]]; then ok_steps=$((ok_steps + 1)); else failed_steps=$((failed_steps + 1)); fi
  }

  # Paso 1: verificar sqlite3
  step_start=$(date +%s%3N)
  if assert_command_exists sqlite3; then
      log_step "assert_command_exists" "check sqlite3" "ok" $(( $(date +%s%3N) - step_start ))
  else
      log_step "assert_command_exists" "check sqlite3" "error" $(( $(date +%s%3N) - step_start )) "" "sqlite3 not found"
      ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      DURATION=$(( $(date +%s%3N) - START_MS ))
      set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
      exit 1
  fi

  # Paso 2: verificar registry.db
  step_start=$(date +%s%3N)
  if db_size=$(assert_file_exists "$DB_PATH"); then
      log_step "assert_file_exists" "check registry.db" "ok" $(( $(date +%s%3N) - step_start )) "${db_size} bytes"
  else
      log_step "assert_file_exists" "check registry.db" "error" $(( $(date +%s%3N) - step_start )) "" "registry.db not found"
      ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      DURATION=$(( $(date +%s%3N) - START_MS ))
      set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
      exit 1
  fi

  # Paso 3: validar functions
  step_start=$(date +%s%3N)
  fn_broken=$(validate_registry_paths "$DB_PATH" functions "$REGISTRY_ROOT")
  fn_count=$(printf '%s' "$fn_broken" | grep -c . || true)
  log_step "validate_registry_paths" "check functions" "ok" $(( $(date +%s%3N) - step_start )) "$fn_count broken"

  # Paso 4: validar types
  step_start=$(date +%s%3N)
  ty_broken=$(validate_registry_paths "$DB_PATH" types "$REGISTRY_ROOT")
  ty_count=$(printf '%s' "$ty_broken" | grep -c . || true)
  log_step "validate_registry_paths" "check types" "ok" $(( $(date +%s%3N) - step_start )) "$ty_count broken"

  # Paso 5: generar archivo de salida
  step_start=$(date +%s%3N)
  total_broken=$(( fn_count + ty_count ))

  {
      echo "# Broken file_path entries in registry.db"
      echo "# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
      echo "# Total: $total_broken broken paths"
      echo "#"
      echo "# Format: id | file_path (in .md) | domain | table"
      echo "# ---"
      if [[ $total_broken -eq 0 ]]; then
          echo "# All paths are valid."
      else
          if [[ -n "$fn_broken" ]]; then
              echo ""
              echo "## Functions ($fn_count)"
              printf '%s\n' "$fn_broken" | while IFS=$'\t' read -r id fp domain table; do
                  echo "$id | $fp | $domain | $table"
              done
          fi
          if [[ -n "$ty_broken" ]]; then
              echo ""
              echo "## Types ($ty_count)"
              printf '%s\n' "$ty_broken" | while IFS=$'\t' read -r id fp domain table; do
                  echo "$id | $fp | $domain | $table"
              done
          fi
      fi
  } > "$OUTPUT_FILE"

  log_step "write_output" "generate $OUTPUT_FILE" "ok" $(( $(date +%s%3N) - step_start )) "$total_broken entries"

  # Reporte final
  ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  DURATION=$(( $(date +%s%3N) - START_MS ))
  total_steps=$(( ok_steps + failed_steps ))

  if [[ $total_broken -eq 0 ]]; then
      status="success"
  else
      status="partial"
  fi

  set +e
  report_execution_json "audit_registry_paths" "$status" 0 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"
  set -e

  echo ""
  echo "--- Results ---"
  echo "Broken paths: $total_broken"
  echo "Output: $OUTPUT_FILE"

  exit_with_status "$total_steps" "$ok_steps" "$failed_steps" > /dev/null

params_schema:
  {"params":[{"name":"output_file","desc":"archivo de salida con rutas rotas (default: broken_paths.txt en raíz)"}],"output":"sin salida directa; genera archivo con rutas inválidas"}


id: capacitor_build_apk_bash_pipelines
name: capacitor_build_apk
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: capacitor_build_apk(web_app_dir: string, [app_id: string], [app_name: string]) -> void
tags: ["android","apk","capacitor","mobile","build","pipeline","bash"]
uses_functions: ["install_android_sdk_bash_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/capacitor_build_apk.sh
created_at: 2026-04-06T16:40:45Z
updated_at: 2026-04-06T16:40:45Z
props: []
emits: []
variant: []
content_hash: cc0f228c2ba214cc818c23fb7f266173fd25a868215c855d5ab656f0ed4eae9e
description:
  Pipeline que convierte una web app en un APK de Android usando Capacitor. Valida el entorno (ANDROID_HOME, Java 17+), construye el bundle web si no existe dist/, inicializa Capacitor si no está configurado, añade la plataforma Android, sincroniza y compila el APK con Gradle. El APK final queda en el directorio raíz de la web app.

example:
  ```bash
  # Build con defaults (app-id y app-name desde package.json)
  ./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app

  # Build especificando app-id y app-name
  ./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app \
    --app-id com.miempresa.miapp \
    --app-name "Mi Aplicación"
  ```

notes:
  El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecución inmediatamente.

  El APK generado es un **debug build**, apto para desarrollo y pruebas. Para publicar en Play Store se necesita un release build firmado (`assembleRelease` con un keystore).

  `install_android_sdk_bash_infra` se referencia como dependencia previa: el usuario debe haberlo ejecutado (o haber instalado el SDK manualmente) antes de invocar este pipeline.

  La detección del `app_name` desde `package.json` usa `node -e` inline, lo que requiere que Node.js esté disponible. Si el campo `name` no existe en el JSON, se usa el valor por defecto `app`.

  Para instalar el APK en un dispositivo Android conectado por USB (con depuración USB activada):

  ```bash
  adb install <web_app_dir>/<app_name>.apk
  ```

documentation:
  ## Flujo

  1. **Validación** — verifica que `web_app_dir` existe, tiene `package.json`, que `ANDROID_HOME` está seteado (o sourcea `$HOME/android-sdk/env.sh`) y que Java 17+ está disponible.
  2. **Build web** — si no existe `dist/`, ejecuta `pnpm build` en el directorio de la app.
  3. **Init Capacitor** — si no existe `capacitor.config.ts`, instala `@capacitor/core`, `@capacitor/cli` y `@capacitor/android` via npm y genera el archivo de configuración con el `appId`, `appName` y `webDir: dist`.
  4. **Add Android** — si no existe el directorio `android/`, ejecuta `npx cap add android`.
  5. **Sync** — ejecuta `npx cap sync android` para copiar los assets web al proyecto Android.
  6. **Build APK** — ejecuta `./gradlew assembleDebug` desde `android/`; si falla sale con exit 1.
  7. **Copia APK** — copia `android/app/build/outputs/apk/debug/app-debug.apk` a `<web_app_dir>/<app_name>.apk`.
  8. **Resultado** — imprime la ruta del APK y su tamaño en MB.

  ## Requisitos

  - **Node.js** y **pnpm** disponibles en PATH
  - **Java 17+** disponible en PATH
  - **Android SDK** instalado: `ANDROID_HOME` seteado, o bien `$HOME/android-sdk/env.sh` existente (generado por `install_android_sdk`)
  - **Gradle wrapper** presente en el directorio `android/` (generado por `cap add android`)

code:
  #!/usr/bin/env bash
  # capacitor_build_apk
  # -------------------
  # Pipeline que convierte una web app buildeada en un APK de Android usando Capacitor.
  # Asume que el Android SDK está instalado (via install_android_sdk o manualmente).
  #
  # USO:
  #   ./capacitor_build_apk.sh <web_app_dir> [--app-id com.example.app] [--app-name "My App"]
  #
  # ARGUMENTOS:
  #   web_app_dir         Directorio de la web app (debe contener package.json)
  #   --app-id            ID de la app Android (default: com.fnregistry.app)
  #   --app-name          Nombre visible de la app (default: name de package.json)
  #
  # REQUISITOS:
  #   - Node.js + pnpm instalados en PATH
  #   - Java 17+ instalado en PATH
  #   - Android SDK: ANDROID_HOME seteado o $HOME/android-sdk/env.sh disponible

  set -euo pipefail

  # ---------------------------------------------------------------------------
  # Parseo de argumentos
  # ---------------------------------------------------------------------------

  WEB_APP_DIR=""
  APP_ID="com.fnregistry.app"
  APP_NAME=""

  while [[ $# -gt 0 ]]; do
      case "$1" in
          --app-id)
              APP_ID="$2"
              shift 2
              ;;
          --app-name)
              APP_NAME="$2"
              shift 2
              ;;
          -*)
              echo "[capacitor_build_apk] ERROR: argumento desconocido: $1" >&2
              echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
              exit 1
              ;;
          *)
              WEB_APP_DIR="$1"
              shift
              ;;
      esac
  done

  if [[ -z "$WEB_APP_DIR" ]]; then
      echo "[capacitor_build_apk] ERROR: web_app_dir es obligatorio." >&2
      echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
      exit 1
  fi

  # ---------------------------------------------------------------------------
  # 1. Validación
  # ---------------------------------------------------------------------------

  echo "[capacitor_build_apk] Validando entorno..."

  # Verificar que web_app_dir existe y tiene package.json
  if [[ ! -d "$WEB_APP_DIR" ]]; then
      echo "[capacitor_build_apk] ERROR: directorio no existe: $WEB_APP_DIR" >&2
      exit 1
  fi

  if [[ ! -f "$WEB_APP_DIR/package.json" ]]; then
      echo "[capacitor_build_apk] ERROR: no se encontró package.json en $WEB_APP_DIR" >&2
      exit 1
  fi

  # Resolver app name desde package.json si no se pasó
  if [[ -z "$APP_NAME" ]]; then
      APP_NAME=$(node -e "const p = require('$WEB_APP_DIR/package.json'); process.stdout.write(p.name || 'app');" 2>/dev/null || echo "app")
      echo "[capacitor_build_apk] App name detectado desde package.json: $APP_NAME"
  fi

  # Verificar ANDROID_HOME o sourcea env.sh
  if [[ -z "${ANDROID_HOME:-}" ]]; then
      ANDROID_ENV="$HOME/android-sdk/env.sh"
      if [[ -f "$ANDROID_ENV" ]]; then
          echo "[capacitor_build_apk] ANDROID_HOME no seteado, sourceando $ANDROID_ENV ..."
          # shellcheck source=/dev/null
          source "$ANDROID_ENV"
      else
          echo "[capacitor_build_apk] ERROR: ANDROID_HOME no está seteado y no se encontró $ANDROID_ENV" >&2
          echo "  Instala el SDK con install_android_sdk o setea ANDROID_HOME manualmente." >&2
          exit 1
      fi
  fi

  echo "[capacitor_build_apk] ANDROID_HOME: $ANDROID_HOME"

  # Verificar Java 17+
  if ! command -v java &>/dev/null; then
      echo "[capacitor_build_apk] ERROR: java no está en PATH." >&2
      exit 1
  fi

  JAVA_VERSION=$(java -version 2>&1 | head -1 | grep -oP '(?<=version ")([0-9]+)' | head -1 || echo "0")
  if [[ "$JAVA_VERSION" -lt 17 ]]; then
      echo "[capacitor_build_apk] ERROR: se requiere Java 17+. Versión detectada: $JAVA_VERSION" >&2
      exit 1
  fi

  echo "[capacitor_build_apk] Java $JAVA_VERSION detectado."

  # ---------------------------------------------------------------------------
  # 2. Build web (si no existe dist/)
  # ---------------------------------------------------------------------------

  if [[ ! -d "$WEB_APP_DIR/dist" ]]; then
      echo "[capacitor_build_apk] No se encontró dist/, ejecutando pnpm build..."
      (cd "$WEB_APP_DIR" && pnpm build)
      echo "[capacitor_build_apk] Build web completado."
  else
      echo "[capacitor_build_apk] dist/ ya existe, omitiendo build web."
  fi

  # ---------------------------------------------------------------------------
  # 3. Init Capacitor (si no existe capacitor.config.ts)
  # ---------------------------------------------------------------------------

  if [[ ! -f "$WEB_APP_DIR/capacitor.config.ts" ]]; then
      echo "[capacitor_build_apk] Instalando dependencias de Capacitor..."
      (cd "$WEB_APP_DIR" && npm install @capacitor/core @capacitor/cli @capacitor/android)

      echo "[capacitor_build_apk] Generando capacitor.config.ts..."
      cat > "$WEB_APP_DIR/capacitor.config.ts" <<CAPCONFIG
  import type { CapacitorConfig } from '@capacitor/cli';

  const config: CapacitorConfig = {
    appId: '${APP_ID}',
    appName: '${APP_NAME}',
    webDir: 'dist',
    server: { androidScheme: 'https' }
  };

  export default config;
  CAPCONFIG

      echo "[capacitor_build_apk] capacitor.config.ts generado."
  else
      echo "[capacitor_build_apk] capacitor.config.ts ya existe, omitiendo init."
  fi

  # ---------------------------------------------------------------------------
  # 4. Add Android (si no existe el directorio android/)
  # ---------------------------------------------------------------------------

  if [[ ! -d "$WEB_APP_DIR/android" ]]; then
      echo "[capacitor_build_apk] Añadiendo plataforma Android..."
      (cd "$WEB_APP_DIR" && npx cap add android)
      echo "[capacitor_build_apk] Plataforma Android añadida."
  else
      echo "[capacitor_build_apk] Directorio android/ ya existe, omitiendo cap add."
  fi

  # ---------------------------------------------------------------------------
  # 5. Sync
  # ---------------------------------------------------------------------------

  echo "[capacitor_build_apk] Sincronizando assets web con Android..."
  (cd "$WEB_APP_DIR" && npx cap sync android)
  echo "[capacitor_build_apk] Sync completado."

  # ---------------------------------------------------------------------------
  # 6. Build APK
  # ---------------------------------------------------------------------------

  echo "[capacitor_build_apk] Compilando APK con Gradle..."
  if ! (cd "$WEB_APP_DIR/android" && ./gradlew assembleDebug); then
      echo "[capacitor_build_apk] ERROR: Gradle falló. Revisa los logs anteriores." >&2
      exit 1
  fi

  APK_SOURCE="$WEB_APP_DIR/android/app/build/outputs/apk/debug/app-debug.apk"

  if [[ ! -f "$APK_SOURCE" ]]; then
      echo "[capacitor_build_apk] ERROR: Gradle terminó sin error pero no se encontró el APK en $APK_SOURCE" >&2
      exit 1
  fi

  # ---------------------------------------------------------------------------
  # 7. Copia APK al directorio raíz
  # ---------------------------------------------------------------------------

  APK_DEST="$WEB_APP_DIR/${APP_NAME}.apk"
  cp "$APK_SOURCE" "$APK_DEST"

  # ---------------------------------------------------------------------------
  # 8. Resultado
  # ---------------------------------------------------------------------------

  APK_SIZE_BYTES=$(stat -c%s "$APK_DEST" 2>/dev/null || stat -f%z "$APK_DEST" 2>/dev/null || echo "0")
  APK_SIZE_MB=$(awk "BEGIN {printf \"%.1f\", $APK_SIZE_BYTES/1048576}")

  echo ""
  echo "---------------------------------------------------------------------"
  echo "APK generado: $APK_DEST"
  echo "Tamaño: ${APK_SIZE_MB} MB"
  echo ""
  echo "Para instalar en un dispositivo conectado por USB:"
  echo "  adb install '$APK_DEST'"
  echo "---------------------------------------------------------------------"

params_schema:
  {"params":[{"name":"web_app_dir","desc":"directorio raíz de la web app; debe contener package.json; si no existe dist/ se ejecuta pnpm build automáticamente"},{"name":"app_id","desc":"identificador de la app Android en formato reverse-DNS (default: com.fnregistry.app)"},{"name":"app_name","desc":"nombre visible de la app Android; si se omite, se lee del campo name de package.json"}],"output":"APK de debug en \u003cweb_app_dir\u003e/\u003capp_name\u003e.apk; imprime ruta y tamaño en MB al finalizar"}


id: export_analysis_pdfs_bash_pipelines
name: export_analysis_pdfs
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: export_analysis_pdfs(nombre: string, [pattern: string]) -> void
tags: ["pipeline","jupyter","pdf","export","nbconvert","launcher"]
uses_functions: ["assert_command_exists_bash_shell","install_nbconvert_bash_infra","notebook_to_pdf_bash_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/export_analysis_pdfs.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 547c15c27db0fa782bf3a11e72024e9a2634652f25e17f5790cb3378f3fc39bd
description:
  Exporta todos los notebooks de un analisis Jupyter a PDF. Instala nbconvert y playwright automaticamente si no estan presentes.

example:
  ```bash
  # Exportar todos los notebooks de un analisis
  ./export_analysis_pdfs.sh finanzas

  # Con pattern especifico
  ./export_analysis_pdfs.sh ml "notebooks/01_*.ipynb"

  # Via fn run
  fn run export_analysis_pdfs finanzas
  fn run export_analysis_pdfs ml "notebooks/01_*.ipynb"
  ```

notes:
  El analysis debe existir previamente en `analysis/{nombre}/` con un venv inicializado. Los PDFs se generan en `analysis/{nombre}/notebooks/pdf/` por defecto. El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecucion.

documentation:
  ## Flujo

  1. `assert_command_exists uv` — verifica que uv esta disponible
  2. `install_nbconvert` — instala nbconvert y playwright con chromium (idempotente)
  3. `notebook_to_pdf` — convierte notebooks al patron indicado a PDF en `notebooks/pdf/`

code:
  #!/usr/bin/env bash
  # export_analysis_pdfs
  # ---------------------
  # Pipeline que exporta todos los notebooks de un analisis a PDF.
  # Compone: assert_command_exists + install_nbconvert + notebook_to_pdf
  #
  # USO:
  #   ./export_analysis_pdfs.sh <nombre_analysis> [pattern]
  #
  # EJEMPLOS:
  #   ./export_analysis_pdfs.sh finanzas
  #   ./export_analysis_pdfs.sh ml "notebooks/01_*.ipynb"

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  # Source funciones atomicas
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/install_nbconvert.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/notebook_to_pdf.sh"

  # ── Argumentos ──────────────────────────────────────────────

  NOMBRE="${1:-}"
  if [ -z "$NOMBRE" ]; then
      echo "Uso: $0 <nombre_analysis> [pattern]" >&2
      echo "  Ejemplo: $0 finanzas" >&2
      echo "  Ejemplo: $0 ml 'notebooks/01_*.ipynb'" >&2
      exit 1
  fi
  shift
  PATTERN="${1:-notebooks/*.ipynb}"

  ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}"

  # Verificar que el analysis existe
  if [ ! -d "$ANALYSIS_DIR" ]; then
      echo "Error: analysis '${NOMBRE}' no existe en ${ANALYSIS_DIR}" >&2
      exit 1
  fi

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  EXPORT ANALYSIS PDFs: ${NOMBRE}"
  echo "  Directorio: ${ANALYSIS_DIR}"
  echo "════════════════════════════════════════════════════════════"
  echo ""

  # ── 1. Verificar herramientas ───────────────────────────────

  echo "[1/3] Verificando herramientas..."
  assert_command_exists uv
  echo "  OK"

  # ── 2. Instalar nbconvert + playwright ──────────────────────

  echo "[2/3] Instalando dependencias de exportacion..."
  install_nbconvert "$ANALYSIS_DIR"
  echo "  OK"

  # ── 3. Convertir notebooks a PDF ────────────────────────────

  echo "[3/3] Convirtiendo notebooks a PDF..."
  notebook_to_pdf "$ANALYSIS_DIR" "$PATTERN"

  # ── Resumen ─────────────────────────────────────────────────

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  EXPORT COMPLETADO"
  echo "════════════════════════════════════════════════════════════"

params_schema:
  {"params":[{"name":"nombre","desc":"nombre del análisis en analysis/{nombre}/"},{"name":"pattern","desc":"glob de notebooks a exportar (default: notebooks/*.ipynb)"}],"output":"sin salida directa; genera PDFs en notebooks/pdf/"}


id: gitea_init_app_bash_pipelines
name: gitea_init_app
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: gitea_init_app(directory: string, owner: string, name: string, private: string) -> string
tags: ["gitea","git","pipeline","repo","create","push","launcher","infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/gitea_init_app.sh
created_at: 2026-04-05T23:01:37Z
updated_at: 2026-04-05T23:01:37Z
props: []
emits: []
variant: []
content_hash: 6fad8d2b0ff043b9697e43304b184c7f85d64cdb555bf372d8ccdaaeea4a89df
description:
  Pipeline que crea un repositorio en Gitea, sube el directorio local y añade a egutierrez como colaborador admin. Compone gitea_create_repo → gitea_push_directory → gitea_add_collaborator.

uses_functions:
  ["gitea_create_repo_bash_infra","gitea_push_directory_bash_infra","gitea_add_collaborator_bash_infra"]

example:
  ```bash
  export GITEA_URL="$(pass agentes/gitea-url)"
  export GITEA_TOKEN="$(pass agentes/dataforge-token)"

  # Crear repo con nombre inferido del directorio
  bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg

  # Nombre explícito y repo privado
  bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg my-custom-name true

  # Con flags
  bash bash/functions/pipelines/gitea_init_app.sh \
      --directory /home/lucas/myapp \
      --owner myorg \
      --name my-app \
      --private true
  ```

notes:
  - Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
  - Si el repo ya existe (409), el pipeline continúa con el push y añade el colaborador.
  - El colaborador `egutierrez` es fijo en el pipeline — para variarlo usar las funciones individuales.
  - La URL del repo se imprime a stdout al finalizar.

documentation:
  ## Pasos del pipeline

  1. `gitea_create_repo owner name private` — crea el repo (idempotente si ya existe)
  2. `gitea_push_directory directory owner repo` — inicializa git y hace push del directorio
  3. `gitea_add_collaborator owner repo egutierrez admin` — añade colaborador con permisos admin

code:
  #!/usr/bin/env bash
  # Pipeline: gitea_init_app — Crea repo en Gitea, sube directorio y añade colaborador egutierrez
  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR/../infra/gitea_create_repo.sh"
  source "$SCRIPT_DIR/../infra/gitea_push_directory.sh"
  source "$SCRIPT_DIR/../infra/gitea_add_collaborator.sh"

  main() {
      local directory=""
      local owner=""
      local name=""
      local private="false"

      # Parsear argumentos
      while [[ $# -gt 0 ]]; do
          case "$1" in
              --directory) directory="$2"; shift 2 ;;
              --owner)     owner="$2";     shift 2 ;;
              --name)      name="$2";      shift 2 ;;
              --private)   private="$2";   shift 2 ;;
              *)
                  # Argumentos posicionales: directory owner [name] [private]
                  if [[ -z "$directory" ]]; then
                      directory="$1"
                  elif [[ -z "$owner" ]]; then
                      owner="$1"
                  elif [[ -z "$name" ]]; then
                      name="$1"
                  else
                      private="$1"
                  fi
                  shift
                  ;;
          esac
      done

      if [[ -z "$directory" || -z "$owner" ]]; then
          echo "gitea_init_app: uso: gitea_init_app <directory> <owner> [name] [private]" >&2
          echo "gitea_init_app: o con flags: --directory <dir> --owner <owner> [--name <name>] [--private true]" >&2
          return 1
      fi

      # Inferir nombre del repo desde basename del directorio si no se especificó
      if [[ -z "$name" ]]; then
          name=$(basename "$directory")
          echo "gitea_init_app: nombre inferido del directorio: '$name'" >&2
      fi

      if [[ -z "${GITEA_URL:-}" ]]; then
          echo "gitea_init_app: GITEA_URL no está seteada" >&2
          return 1
      fi
      if [[ -z "${GITEA_TOKEN:-}" ]]; then
          echo "gitea_init_app: GITEA_TOKEN no está seteado" >&2
          return 1
      fi

      echo "gitea_init_app: iniciando pipeline para '$owner/$name'..." >&2
      echo "gitea_init_app: directorio fuente: '$directory'" >&2

      # Paso 1: Crear repo
      echo "gitea_init_app: [1/3] creando repositorio..." >&2
      gitea_create_repo "$owner" "$name" "$private" "" > /dev/null

      # Paso 2: Subir directorio
      echo "gitea_init_app: [2/3] subiendo directorio al repositorio..." >&2
      gitea_push_directory "$directory" "$owner" "$name"

      # Paso 3: Añadir colaborador egutierrez con permisos admin
      echo "gitea_init_app: [3/3] añadiendo colaborador egutierrez..." >&2
      gitea_add_collaborator "$owner" "$name" "egutierrez" "admin"

      echo "gitea_init_app: pipeline completado — ${GITEA_URL}/${owner}/${name}" >&2
      echo "${GITEA_URL}/${owner}/${name}"
  }

  main "$@"

params_schema:
  {"params":[{"name":"directory","desc":"ruta al directorio local a subir como repositorio"},{"name":"owner","desc":"usuario u organización en Gitea que será propietaria del repo"},{"name":"name","desc":"nombre del repositorio (opcional: se infiere del basename del directorio)"},{"name":"private","desc":"si el repo debe ser privado, 'true' o 'false' (default: false)"}],"output":"URL del repositorio creado en Gitea"}


id: init_go_module_bash_pipelines
name: init_go_module
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: init_go_module([module_path: string]) -> void
tags: ["bash","go","module","init","scaffold","launcher"]
uses_functions: ["install_go_bash_infra","assert_command_exists_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/init_go_module.sh
created_at: 2026-04-08T21:30:10Z
updated_at: 2026-04-08T21:30:10Z
props: []
emits: []
variant: []
content_hash: a4ef6ad54b0de42c8a4df397a5fb6a9a8cea4abece1efd9e1dc4bd34e1870740
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/inicializar_repos/go/init_go_module.sh
description:
  Pipeline que inicializa un módulo Go simple en el directorio actual. Crea go.mod (go mod init), main.go con hello world, .gitignore, build.sh (cross-compilation linux/windows/all) y dev.sh para ejecución rápida.

example:
  ```bash
  mkdir mi-modulo && cd mi-modulo

  # Con module_path automático (github.com/user/mi-modulo)
  bash bash/functions/pipelines/init_go_module.sh

  # Con module_path explícito
  bash bash/functions/pipelines/init_go_module.sh github.com/miorg/mi-modulo

  # Ejecutar tras crear
  ./dev.sh
  ./build.sh
  ./build.sh all
  ```

notes:
  Crea un módulo minimalista (main.go simple + build tools). Para proyectos con estructura profesional (cmd/, internal/, pkg/) usar `init_go_project`. No inicializa git — añadido manualmente o via gitea_init_app.

code:
  #!/usr/bin/env bash
  # init_go_module
  # --------------
  # Pipeline que inicializa un módulo Go simple en el directorio actual.
  # Crea: go.mod, main.go, .gitignore, build.sh y dev.sh.
  #
  # USO:
  #   bash init_go_module.sh [module_path]
  #
  # ARGUMENTOS:
  #   module_path   Path del módulo Go (opcional; default: github.com/user/<dirname>)

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

  init_go_module() {
      local module_path="${1:-}"

      local dir_name
      dir_name="$(basename "$(pwd)")"

      if [[ -z "$module_path" ]]; then
          module_path="github.com/user/${dir_name}"
      fi

      echo "=== Inicializar Módulo Go ==="
      echo "  Módulo:    ${module_path}"
      echo "  Directorio: $(pwd)"
      echo ""

      # Verificar Go
      if ! command -v go &>/dev/null; then
          echo "init_go_module: Go no está instalado" >&2
          return 1
      fi
      echo "Go detectado: $(go version)"
      echo ""

      # Inicializar módulo
      echo "Inicializando go module..."
      if ! go mod init "$module_path"; then
          echo "init_go_module: falló go mod init" >&2
          return 1
      fi
      echo "go.mod creado"
      echo ""

      # Crear main.go
      echo "Creando main.go..."
      cat > main.go << 'GOEOF'
  package main

  import (
  	"fmt"
  )

  func main() {
  	fmt.Println("Hola desde Go!")
  	fmt.Println("Modulo inicializado correctamente")
  }
  GOEOF
      echo "main.go creado"
      echo ""

      # Crear .gitignore
      echo "Creando .gitignore..."
      cat > .gitignore << 'IGNEOF'
  # Binarios
  *.exe
  *.exe~
  *.dll
  *.so
  *.dylib
  bin/
  build/

  # Archivos de test
  *.test
  *.out
  coverage.txt
  *.prof

  # IDEs
  .idea/
  .vscode/
  *.swp
  *.swo
  *~

  # OS
  .DS_Store
  Thumbs.db
  IGNEOF
      echo ".gitignore creado"
      echo ""

      # Crear build.sh
      echo "Creando build.sh..."
      cat > build.sh << 'BUILDEOF'
  #!/bin/bash
  set -euo pipefail

  TARGET="${1:-linux}"
  mkdir -p bin

  case "$TARGET" in
      linux)
          GOOS=linux GOARCH=amd64 go build -o bin/app-linux main.go
          chmod +x bin/app-linux
          echo "Compilado: bin/app-linux"
          ;;
      windows)
          GOOS=windows GOARCH=amd64 go build -o bin/app-windows.exe main.go
          echo "Compilado: bin/app-windows.exe"
          ;;
      all)
          GOOS=linux   GOARCH=amd64 go build -o bin/app-linux        main.go && echo "Linux OK"
          GOOS=windows GOARCH=amd64 go build -o bin/app-windows.exe  main.go && echo "Windows OK"
          GOOS=darwin  GOARCH=amd64 go build -o bin/app-macos        main.go && echo "macOS OK"
          ;;
      *)
          echo "Uso: ./build.sh [linux|windows|all]" >&2
          exit 1
          ;;
  esac
  BUILDEOF
      chmod +x build.sh
      echo "build.sh creado"
      echo ""

      # Crear dev.sh
      echo "Creando dev.sh..."
      cat > dev.sh << 'DEVEOF'
  #!/bin/bash
  set -euo pipefail

  go run main.go "$@"
  DEVEOF
      chmod +x dev.sh
      echo "dev.sh creado"
      echo ""

      echo "=== Modulo Go creado exitosamente ==="
      echo ""
      echo "Archivos generados:"
      echo "  main.go     - Codigo del modulo"
      echo "  go.mod      - Modulo Go (${module_path})"
      echo "  .gitignore  - Exclusiones git"
      echo "  build.sh    - Compilar binario"
      echo "  dev.sh      - Ejecutar directamente"
      echo ""
      echo "Proximos pasos:"
      echo "  ./dev.sh           - Ejecutar el modulo"
      echo "  ./build.sh         - Compilar para Linux"
      echo "  ./build.sh windows - Compilar para Windows"
      echo "  ./build.sh all     - Compilar para todo"
  }

  init_go_module "$@"

params_schema:
  {"params":[{"name":"module_path","desc":"path del módulo Go, ej: github.com/user/mymodule (default: github.com/user/\u003cdirname\u003e)"}],"output":"crea archivos en el directorio actual y muestra progreso; exit code 1 si Go no está instalado o go mod init falla"}


id: init_go_project_bash_pipelines
name: init_go_project
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: init_go_project([module_path: string]) -> void
tags: ["bash","go","project","init","scaffold","professional","launcher"]
uses_functions: ["install_go_bash_infra","assert_command_exists_bash_shell","create_project_structure_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/init_go_project.sh
created_at: 2026-04-08T21:30:10Z
updated_at: 2026-04-08T21:30:10Z
props: []
emits: []
variant: []
content_hash: 0788b07e1a527fe615fcbd7756bcfba949ffbba8321c366896fb9bbfb21dae27
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/inicializar_repos/go/init_go_proyect.sh
description:
  Pipeline que inicializa un repositorio Go completo con estructura profesional: cmd/app, internal/config, internal/service (con tests), pkg/version, scripts (run/test/build/build-all/lint), Makefile, .gitignore, README y git init con git add.

example:
  ```bash
  mkdir mi-proyecto && cd mi-proyecto

  # Con module_path automático
  bash bash/functions/pipelines/init_go_project.sh

  # Con module_path explícito
  bash bash/functions/pipelines/init_go_project.sh github.com/miorg/mi-proyecto

  # Ejecutar y testear tras crear
  ./scripts/run.sh
  ./scripts/test.sh
  make build-all
  ```

notes:
  Genera una arquitectura funcional con separación clara: cmd/ (entrypoint), internal/ (lógica privada), pkg/ (librería pública). Incluye un test de ejemplo en internal/service/. Ejecuta go mod tidy y git init + git add al final. Para módulos simples sin estructura, usar `init_go_module`.

code:
  #!/usr/bin/env bash
  # init_go_project
  # ---------------
  # Pipeline que inicializa un repositorio Go completo con estructura profesional:
  # cmd/app, internal/config, internal/service, pkg/version, scripts/, Makefile,
  # .gitignore, README y git init.
  #
  # USO:
  #   bash init_go_project.sh [module_path]
  #
  # ARGUMENTOS:
  #   module_path   Path del módulo Go (opcional; default: github.com/<whoami>/<dirname>)

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

  init_go_project() {
      local module_path="${1:-}"

      local project_name
      project_name="$(basename "$(pwd)")"

      if [[ -z "$module_path" ]]; then
          module_path="github.com/$(whoami)/${project_name}"
      fi

      echo "=== Inicializar Proyecto Go Completo ==="
      echo "  Proyecto: ${project_name}"
      echo "  Módulo:   ${module_path}"
      echo "  Dir:      $(pwd)"
      echo ""

      # Verificar dependencias
      if ! command -v go &>/dev/null; then
          echo "init_go_project: Go no está instalado" >&2
          return 1
      fi
      if ! command -v git &>/dev/null; then
          echo "init_go_project: git no está instalado" >&2
          return 1
      fi

      echo "Go:  $(go version)"
      echo "git: $(git --version)"
      echo ""

      # Crear estructura de carpetas
      echo "Creando estructura de carpetas..."
      mkdir -p cmd/app internal/config internal/service pkg/version scripts bin
      echo "Estructura base creada"
      echo ""

      # go mod init
      echo "Inicializando módulo Go..."
      if ! go mod init "$module_path"; then
          echo "init_go_project: falló go mod init" >&2
          return 1
      fi
      echo "go.mod creado"
      echo ""

      # Crear archivos fuente
      echo "Creando archivos fuente..."

      cat > cmd/app/main.go << GOEOF
  package main

  import (
  	"fmt"

  	"${module_path}/internal/config"
  	"${module_path}/internal/service"
  	"${module_path}/pkg/version"
  )

  func main() {
  	cfg := config.Load()
  	msg := service.BuildStartupMessage(cfg.AppName, version.Current())
  	fmt.Println(msg)
  }
  GOEOF

      cat > internal/config/config.go << 'CFGEOF'
  package config

  import "os"

  type AppConfig struct {
  	AppName string
  }

  func Load() AppConfig {
  	appName := os.Getenv("APP_NAME")
  	if appName == "" {
  		appName = "Go Project"
  	}
  	return AppConfig{AppName: appName}
  }
  CFGEOF

      cat > internal/service/message.go << 'SVCEOF'
  package service

  import "fmt"

  func BuildStartupMessage(appName, appVersion string) string {
  	return fmt.Sprintf("%s iniciado correctamente (version %s)", appName, appVersion)
  }
  SVCEOF

      cat > internal/service/message_test.go << 'TESTEOF'
  package service

  import "testing"

  func TestBuildStartupMessage(t *testing.T) {
  	result := BuildStartupMessage("MyApp", "0.1.0")
  	want := "MyApp iniciado correctamente (version 0.1.0)"
  	if result != want {
  		t.Fatalf("resultado inesperado:\nwant: %s\ngot:  %s", want, result)
  	}
  }
  TESTEOF

      cat > pkg/version/version.go << 'VEREOF'
  package version

  var value = "0.1.0"

  func Current() string {
  	return value
  }
  VEREOF

      echo "Archivos fuente creados"
      echo ""

      # Makefile
      echo "Creando Makefile..."
      cat > Makefile << 'MAKEEOF'
  APP_NAME=app
  CMD_PATH=./cmd/app
  BIN_DIR=./bin

  .PHONY: run build build-all test fmt tidy clean

  run:
  	go run $(CMD_PATH)

  build:
  	mkdir -p $(BIN_DIR)
  	go build -o $(BIN_DIR)/$(APP_NAME) $(CMD_PATH)

  build-all:
  	bash ./scripts/build-all.sh

  test:
  	go test ./...

  fmt:
  	gofmt -w ./cmd ./internal ./pkg

  tidy:
  	go mod tidy

  clean:
  	rm -rf $(BIN_DIR)
  MAKEEOF
      echo "Makefile creado"
      echo ""

      # .gitignore
      cat > .gitignore << 'IGNEOF'
  bin/
  *.exe
  *.exe~
  *.dll
  *.so
  *.dylib
  *.test
  *.out
  coverage.out
  .idea/
  .vscode/
  *.swp
  *.swo
  *~
  .DS_Store
  Thumbs.db
  IGNEOF

      # README
      cat > README.md << READEOF
  # ${project_name}

  Repositorio Go inicializado con estructura completa.

  ## Uso rápido

  \`\`\`bash
  ./scripts/run.sh
  ./scripts/test.sh
  ./scripts/build.sh
  \`\`\`

  ## Make

  \`\`\`bash
  make run
  make test
  make build
  make build-all
  \`\`\`
  READEOF

      # Scripts
      echo "Creando scripts..."

      cat > scripts/run.sh << 'RUNEOF'
  #!/bin/bash
  set -euo pipefail
  go run ./cmd/app
  RUNEOF

      cat > scripts/test.sh << 'TESTSHEOF'
  #!/bin/bash
  set -euo pipefail
  go test ./...
  TESTSHEOF

      cat > scripts/build.sh << 'BUILDSHEOF'
  #!/bin/bash
  set -euo pipefail
  mkdir -p ./bin
  go build -o ./bin/app ./cmd/app
  echo "Binario: ./bin/app"
  BUILDSHEOF

      cat > scripts/build-all.sh << 'BUILDALLEOF'
  #!/bin/bash
  set -euo pipefail
  mkdir -p ./bin
  GOOS=linux   GOARCH=amd64 go build -o ./bin/app-linux-amd64   ./cmd/app
  GOOS=darwin  GOARCH=amd64 go build -o ./bin/app-darwin-amd64  ./cmd/app
  GOOS=windows GOARCH=amd64 go build -o ./bin/app-windows-amd64.exe ./cmd/app
  echo "Builds en ./bin"
  BUILDALLEOF

      cat > scripts/lint.sh << 'LINTEOF'
  #!/bin/bash
  set -euo pipefail
  unformatted="$(gofmt -l ./cmd ./internal ./pkg)"
  if [ -n "$unformatted" ]; then
      echo "Archivos sin formato:" >&2
      echo "$unformatted" >&2
      echo "Ejecuta: make fmt" >&2
      exit 1
  fi
  echo "Formato OK"
  LINTEOF

      chmod +x scripts/*.sh
      echo "Scripts creados"
      echo ""

      # go mod tidy
      echo "Ajustando dependencias (go mod tidy)..."
      go mod tidy
      echo "Dependencias ajustadas"
      echo ""

      # git init
      echo "Inicializando repositorio git..."
      git init >/dev/null 2>&1
      git add .
      echo "Repositorio git inicializado"
      echo ""

      echo "=== Proyecto Go creado exitosamente ==="
      echo ""
      echo "Estructura:"
      echo "  cmd/app/main.go"
      echo "  internal/config/config.go"
      echo "  internal/service/message.go + message_test.go"
      echo "  pkg/version/version.go"
      echo "  scripts/ (run, test, build, build-all, lint)"
      echo "  Makefile, go.mod, .gitignore, README.md"
      echo ""
      echo "Proximos pasos:"
      echo "  1. ./scripts/run.sh"
      echo "  2. ./scripts/test.sh"
      echo "  3. ./scripts/build-all.sh"
  }

  init_go_project "$@"

params_schema:
  {"params":[{"name":"module_path","desc":"path del módulo Go, ej: github.com/org/myproject (default: github.com/\u003cwhoami\u003e/\u003cdirname\u003e)"}],"output":"crea estructura completa en el directorio actual y muestra progreso; exit code 1 si Go o git no están disponibles"}


id: init_jupyter_analysis_bash_pipelines
name: init_jupyter_analysis
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: init_jupyter_analysis(nombre: string, [...paquetes_extra: string]) -> void
tags: ["jupyter","analysis","setup","pipeline","bash","launcher"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/init_jupyter_analysis.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 9adee852b86ced3705792530a50eebe4a7770de3c37002fc94b8a33dfc77a674
description:
  Inicializa un analisis Jupyter completo en analysis/{nombre}/ con venv, paquetes, launcher, MCP y reglas para agentes Claude. Acepta paquetes extra opcionales.

uses_functions:
  ["assert_command_exists_bash_shell","find_free_port_bash_shell","init_uv_venv_bash_infra","uv_add_packages_bash_infra","write_jupyter_launcher_bash_infra","write_mcp_jupyter_config_bash_infra","write_claude_jupyter_rules_bash_infra","write_jupyter_registry_kernel_bash_infra"]

example:
  ```bash
  # Analisis basico
  ./init_jupyter_analysis.sh finanzas

  # Con paquetes extra
  ./init_jupyter_analysis.sh duckdb polars duckdb
  ./init_jupyter_analysis.sh ml scikit-learn torch

  # Via fn run
  fn run init_jupyter_analysis finanzas
  fn run init_jupyter_analysis ml scikit-learn torch
  ```

notes:
  Cada analisis es independiente (propio venv, propio Jupyter, propio MCP). Mismo patron que `apps/` pero para exploraciones no reutilizables.

  El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecucion.

  Paquetes base siempre incluidos: jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib. Los paquetes extra se añaden a estos.

documentation:
  ## Flujo

  1. `assert_command_exists` — verifica que uv o python3 estan disponibles
  2. Crea estructura `analysis/{nombre}/notebooks/` y `analysis/{nombre}/data/`
  3. `init_uv_venv` — crea venv en `analysis/{nombre}/.venv/`
  4. `uv_add_packages` — instala jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib + extras
  5. `write_jupyter_launcher` — genera `run-jupyter-lab.sh` con modo colaborativo
  6. `find_free_port` + `write_mcp_jupyter_config` — detecta puerto libre y genera `.mcp.json`
  7. `write_claude_jupyter_rules` — genera `.claude/CLAUDE.md` con reglas de agente
  8. `write_jupyter_registry_kernel` — genera IPython startup con `fn_query`, `fn_search`, `fn_code` y acceso a `python/functions/`

code:
  #!/usr/bin/env bash
  # init_jupyter_analysis
  # ----------------------
  # Inicializa un analisis Jupyter completo en analysis/{nombre}/.
  # Compone: assert_command_exists + find_free_port + init_uv_venv +
  #          uv_add_packages + write_jupyter_launcher +
  #          write_mcp_jupyter_config + write_claude_jupyter_rules +
  #          write_jupyter_registry_kernel
  #
  # USO:
  #   ./init_jupyter_analysis.sh <nombre> [paquetes_extra...]
  #
  # EJEMPLOS:
  #   ./init_jupyter_analysis.sh finanzas
  #   ./init_jupyter_analysis.sh duckdb polars duckdb
  #   ./init_jupyter_analysis.sh ml scikit-learn torch

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  # Source funciones atomicas
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/find_free_port.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/init_uv_venv.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/uv_add_packages.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_launcher.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_mcp_jupyter_config.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_claude_jupyter_rules.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_registry_kernel.sh"

  # ── Argumentos ──────────────────────────────────────────────

  NOMBRE="${1:-}"
  if [ -z "$NOMBRE" ]; then
      echo "Uso: $0 <nombre> [paquetes_extra...]" >&2
      echo "  Ejemplo: $0 finanzas polars" >&2
      exit 1
  fi
  shift
  EXTRA_PACKAGES=("$@")

  ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}"

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  INIT JUPYTER ANALYSIS: ${NOMBRE}"
  echo "  Directorio: ${ANALYSIS_DIR}"
  echo "════════════════════════════════════════════════════════════"
  echo ""

  # ── 1. Verificar herramientas ───────────────────────────────

  echo "[1/8] Verificando herramientas..."
  assert_command_exists uv || assert_command_exists python3
  echo "  OK"

  # ── 2. Crear estructura de carpetas ─────────────────────────

  echo "[2/8] Creando estructura..."
  mkdir -p "$ANALYSIS_DIR/notebooks" "$ANALYSIS_DIR/data"
  echo "  ${ANALYSIS_DIR}/notebooks/"
  echo "  ${ANALYSIS_DIR}/data/"

  # ── 3. Crear venv ───────────────────────────────────────────

  echo "[3/8] Inicializando venv..."
  venv_path=$(init_uv_venv "$ANALYSIS_DIR")
  echo "  $venv_path"

  # ── 4. Instalar paquetes ────────────────────────────────────

  echo "[4/8] Instalando paquetes..."
  BASE_PACKAGES=(jupyter jupyterlab jupyter-collaboration jupyter-mcp-server pandas numpy matplotlib)
  ALL_PACKAGES=("${BASE_PACKAGES[@]}" "${EXTRA_PACKAGES[@]}")
  uv_add_packages "$ANALYSIS_DIR" "${ALL_PACKAGES[@]}"
  echo "  Instalados: ${ALL_PACKAGES[*]}"

  # ── 5. Generar launcher ─────────────────────────────────────

  echo "[5/8] Generando launcher..."
  launcher=$(write_jupyter_launcher "$ANALYSIS_DIR")
  echo "  $launcher"

  # ── 6. Configurar MCP ───────────────────────────────────────

  echo "[6/8] Configurando MCP..."
  port=$(find_free_port 8888 8899)
  mcp_config=$(write_mcp_jupyter_config "$ANALYSIS_DIR" "$port")
  echo "  $mcp_config (puerto: $port)"

  # ── 7. Reglas para agentes ──────────────────────────────────

  echo "[7/8] Escribiendo reglas Claude..."
  rules=$(write_claude_jupyter_rules "$ANALYSIS_DIR")
  echo "  $rules"

  # ── 8. Kernel startup con acceso al registry ────────────────

  echo "[8/8] Configurando kernel con acceso al registry..."
  kernel_startup=$(write_jupyter_registry_kernel "$ANALYSIS_DIR")
  echo "  $kernel_startup"

  # ── Resumen ─────────────────────────────────────────────────

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  ANALISIS '${NOMBRE}' LISTO"
  echo "════════════════════════════════════════════════════════════"
  echo ""
  echo "  Pasos siguientes:"
  echo ""
  echo "  1. En otra terminal:"
  echo "     cd ${ANALYSIS_DIR} && ./run-jupyter-lab.sh"
  echo ""
  echo "  2. Abrir Claude en el analisis:"
  echo "     cd ${ANALYSIS_DIR} && claude"
  echo ""
  echo "  3. Abrir en navegador: http://localhost:${port}"
  echo ""
  echo "  FN_REGISTRY_ROOT=${REGISTRY_ROOT}"
  echo ""

params_schema:
  {"params":[{"name":"nombre","desc":"nombre del análisis a crear"},{"name":"paquetes_extra","desc":"paquetes Python adicionales a instalar (variadic, opcional)"}],"output":"sin salida directa; estructura completa en analysis/{nombre}/"}


id: setup_metabase_volume_bash_pipelines
name: setup_metabase_volume
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
tags: ["metabase","docker","setup","launcher","pipeline","bash","infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/setup_metabase_volume.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1ef3a6f96c128f50b07f048588f672cf12c2efe339d2bb00438ab7aa0054674f
signature:
  setup_metabase_volume([registry_db_path: string], [container_name: string], [dest_path: string]) -> void

description:
  Copia registry.db al contenedor Docker de Metabase verificando existencia del archivo, disponibilidad de docker, estado del contenedor y coincidencia de tamaños. Todos los argumentos son opcionales con defaults razonables.

uses_functions:
  ["assert_file_exists_bash_shell","assert_command_exists_bash_shell","assert_docker_container_running_bash_infra","docker_cp_file_bash_infra"]

example:
  ```bash
  # Con defaults
  ./functions/pipelines/setup_metabase_volume.sh

  # Con argumentos explícitos
  ./functions/pipelines/setup_metabase_volume.sh \
    /home/lucas/fn_registry/registry.db \
    metabase \
    /registry.db
  ```

notes:
  El pipeline usa `set -euo pipefail` — cualquier fallo en una función individual detiene la ejecución.

  Las funciones individuales se sourcean desde sus rutas en el registry, relativas a `REGISTRY_ROOT` detectado automáticamente desde la ubicación del script.

  Defaults:
  - `REGISTRY_DB_PATH`: `/home/lucas/fn_registry/registry.db`
  - `CONTAINER_NAME`: `metabase`
  - `DEST_PATH`: `/registry.db`

  Nota de persistencia: `docker cp` copia al contenedor en ejecución. Si el contenedor se reinicia, el archivo se pierde. Para persistencia real, montar el directorio como volumen en docker-compose:

  ```yaml
  volumes:
    - /home/lucas/fn_registry:/fn_registry:ro
  ```

  Y usar `--registry-db-path /fn_registry/registry.db`.

documentation:
  ## Flujo

  1. `assert_file_exists` — verifica que `registry.db` existe localmente y obtiene su tamaño
  2. `assert_command_exists` — verifica que `docker` está disponible en PATH
  3. `assert_docker_container_running` — verifica que el contenedor destino está activo; si falla, muestra lista de contenedores activos
  4. `docker_cp_file` — ejecuta `docker cp` y verifica que los tamaños local y remoto coinciden

code:
  #!/usr/bin/env bash
  # setup_metabase_volume
  # ---------------------
  # Copia registry.db al contenedor Docker de Metabase.
  # Compone: assert_file_exists + assert_command_exists +
  #          assert_docker_container_running + docker_cp_file
  #
  # USO:
  #   ./setup_metabase_volume.sh [REGISTRY_DB_PATH] [CONTAINER_NAME] [DEST_PATH]
  #
  # ARGUMENTOS (opcionales, con defaults):
  #   REGISTRY_DB_PATH  Ruta local al registry.db
  #                     Default: /home/lucas/fn_registry/registry.db
  #   CONTAINER_NAME    Nombre del contenedor Docker de Metabase
  #                     Default: metabase
  #   DEST_PATH         Ruta destino dentro del contenedor
  #                     Default: /registry.db

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/assert_docker_container_running.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/docker_cp_file.sh"

  REGISTRY_DB_PATH="${1:-/home/lucas/fn_registry/registry.db}"
  CONTAINER_NAME="${2:-metabase}"
  DEST_PATH="${3:-/registry.db}"

  echo "[setup_metabase_volume] Configuracion:"
  echo "  registry.db local : $REGISTRY_DB_PATH"
  echo "  contenedor Docker  : $CONTAINER_NAME"
  echo "  ruta en contenedor : $DEST_PATH"
  echo ""

  # 1. Verificar archivo local
  local_size=$(assert_file_exists "$REGISTRY_DB_PATH")
  echo "[setup_metabase_volume] Archivo local encontrado: ${local_size} bytes"

  # 2. Verificar que docker esta disponible
  assert_command_exists docker
  echo "[setup_metabase_volume] docker disponible en PATH."

  # 3. Verificar que el contenedor esta corriendo
  if ! assert_docker_container_running "$CONTAINER_NAME"; then
      echo "" >&2
      echo "Contenedores activos:" >&2
      docker ps --format "  {{.Names}}\t{{.Status}}\t{{.Image}}" >&2
      echo "" >&2
      echo "Si el contenedor se llama diferente, pasa el nombre como segundo argumento:" >&2
      echo "  ./setup_metabase_volume.sh $REGISTRY_DB_PATH <nombre_contenedor>" >&2
      exit 1
  fi
  echo "[setup_metabase_volume] Contenedor '$CONTAINER_NAME' encontrado y activo."

  # 4. Copiar archivo y verificar tamaños
  echo "[setup_metabase_volume] Copiando $REGISTRY_DB_PATH -> ${CONTAINER_NAME}:${DEST_PATH} ..."
  result=$(docker_cp_file "$REGISTRY_DB_PATH" "$CONTAINER_NAME" "$DEST_PATH")

  remote_size=$(echo "$result" | grep -o '"remote_size":[0-9]*' | cut -d: -f2)
  echo "[setup_metabase_volume] OK Copia completada y verificada."
  echo "[setup_metabase_volume] Tamanio: ${local_size} bytes (local) = ${remote_size} bytes (remoto)"

  echo ""
  echo "---------------------------------------------------------------------"
  echo "registry.db disponible en el contenedor como: $DEST_PATH"
  echo ""
  echo "Ahora ejecuta main.py con:"
  echo ""
  echo "  METABASE_ADMIN_PASSWORD=<password> \\"
  echo "  REGISTRY_DB_PATH=${DEST_PATH} \\"
  echo "  python apps/metabase_registry/main.py"
  echo ""
  echo "O bien:"
  echo ""
  echo "  python apps/metabase_registry/main.py \\"
  echo "    --admin-password <password> \\"
  echo "    --registry-db-path ${DEST_PATH}"
  echo "---------------------------------------------------------------------"

params_schema:
  {"params":[{"name":"registry_db_path","desc":"ruta a registry.db local (default: /home/lucas/fn_registry/registry.db)"},{"name":"container_name","desc":"nombre del contenedor Metabase (default: metabase)"},{"name":"dest_path","desc":"ruta destino dentro del contenedor (default: /registry.db)"}],"output":"sin salida directa; verifica transferencia con coincidencia de tamaños"}


id: assert_command_exists_bash_shell
name: assert_command_exists
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: assert_command_exists(command_name: string) -> void
tags: ["assert","command","exists","validation","shell","bash","path"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/assert_command_exists.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5c4e73149cf38e36946509c40ea013491b38c991beb820f767fa14edc66fd615
description:
  Verifica que un comando está disponible en el PATH. Sale con exit code 1 si no se encuentra, con mensaje a stderr.

example:
  ```bash
  source functions/shell/assert_command_exists.sh

  assert_command_exists docker
  assert_command_exists jq
  ```

notes:
  Usa `command -v` (POSIX) con redirección `&>/dev/null` para suprimir output. No produce nada a stdout en caso de éxito.

  Output limpio: void en éxito, mensaje a stderr en fallo.

code:
  # assert_command_exists
  # ---------------------
  # Verifica que un comando está disponible en el PATH.
  # No produce output a stdout.
  # Sale con exit code 1 si el comando no se encuentra.
  #
  # USO (sourced):
  #   source assert_command_exists.sh
  #   assert_command_exists docker

  assert_command_exists() {
      local command_name="$1"

      if ! command -v "$command_name" &>/dev/null; then
          echo "assert_command_exists: comando no encontrado en PATH: $command_name" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"command_name","desc":"nombre del comando a verificar en PATH"}],"output":"sin salida en éxito"}


id: assert_file_exists_bash_shell
name: assert_file_exists
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: assert_file_exists(file_path: string) -> string
tags: ["assert","file","exists","validation","shell","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/assert_file_exists.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 4183e99894fa00ecc4c48143268e35993f5a109593be7edea88ad3b9473ac4b3
description:
  Verifica que un archivo existe en el filesystem. Imprime su tamaño en bytes a stdout. Sale con exit code 1 si el archivo no existe.

example:
  ```bash
  source functions/shell/assert_file_exists.sh

  size=$(assert_file_exists /home/lucas/fn_registry/registry.db)
  echo "Tamaño: $size bytes"
  ```

notes:
  La función se sourcea, no se ejecuta directamente. Usa `stat -c%s` para obtener el tamaño en bytes (compatible con GNU coreutils / Linux).

  Output limpio: solo el número de bytes a stdout. Los errores van a stderr.

  No usa `set -e` internamente — el caller controla el flujo con el exit code de retorno.

code:
  # assert_file_exists
  # ------------------
  # Verifica que un archivo existe en el filesystem.
  # Imprime su tamaño en bytes a stdout.
  # Sale con exit code 1 si el archivo no existe.
  #
  # USO (sourced):
  #   source assert_file_exists.sh
  #   size=$(assert_file_exists /ruta/al/archivo)

  assert_file_exists() {
      local file_path="$1"

      if [ ! -f "$file_path" ]; then
          echo "assert_file_exists: archivo no encontrado: $file_path" >&2
          return 1
      fi

      stat -c%s "$file_path"
  }

params_schema:
  {"params":[{"name":"file_path","desc":"ruta del archivo a verificar"}],"output":"tamaño del archivo en bytes"}


id: bash_check_deps_bash_shell
name: bash_check_deps
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
tags: ["bash","check","dependency","command","exists","validation"]
uses_functions: ["bash_log_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_check_deps.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 07bf7bfb25ab32c999571bdc252acdc4064d9562dec1053705b8057029e4a49e
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
signature:
  check_command(cmd: string, [error_code: string], [description: string]) -> void; check_commands(cmd...: string) -> void; check_directory(dir: string, [msg: string]) -> void; check_file(file: string, [msg: string]) -> void

description:
  Verifica existencia de comandos, directorios y archivos con output formateado. Complementa assert_command_exists con mensajes de error detallados y logging.

example:
  ```bash
  source bash/functions/shell/bash_check_deps.sh

  check_command "docker" "DOCKER_NOT_FOUND" "Docker no esta instalado"
  check_commands "git" "curl" "jq"
  check_directory "/var/data" "El directorio de datos no existe"
  check_file "/etc/config.yaml" "Falta el archivo de configuracion"
  ```

notes:
  `check_command` acepta un error_code y descripcion opcionales para mensajes mas descriptivos que `assert_command_exists`. Usa `debug` internamente para loggear cada verificacion.

  `check_commands` verifica multiples comandos en una sola llamada y reporta todos los faltantes antes de retornar 1.

  Sourcea `bash_log.sh` automaticamente, que a su vez sourcea `bash_colors.sh`. No es necesario sourcea dependencias por separado.

code:
  # bash_check_deps
  # ---------------
  # Verifica existencia de comandos, directorios y archivos.
  # Output formateado con colores via bash_log.
  #
  # USO (sourced):
  #   source bash_check_deps.sh
  #   check_command "docker" "DOCKER_NOT_FOUND" "Docker no esta instalado"
  #   check_commands "git" "curl" "jq"
  #   check_directory "/path/to/dir"
  #   check_file "/path/to/file"

  SCRIPT_DIR_BASH_CHECK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_CHECK/bash_log.sh"
  bash_log_init

  check_command() {
      local cmd="$1"
      local error_code="${2:-COMMAND_NOT_FOUND}"
      local description="${3:-El comando '$cmd' no esta disponible}"

      debug "Verificando comando: $cmd"

      if ! command -v "$cmd" &> /dev/null; then
          error "$description ($error_code)"
          return 1
      fi

      return 0
  }

  check_commands() {
      local failed=0
      for cmd in "$@"; do
          if ! command -v "$cmd" &> /dev/null; then
              error "Comando no encontrado: $cmd"
              failed=1
          fi
      done

      if [ $failed -eq 1 ]; then
          error "Faltan multiples dependencias requeridas"
          return 1
      fi

      return 0
  }

  check_directory() {
      local dir="$1"
      local error_msg="${2:-El directorio '$dir' no existe}"

      debug "Verificando directorio: $dir"

      if [ ! -d "$dir" ]; then
          error "$error_msg"
          return 1
      fi

      return 0
  }

  check_file() {
      local file="$1"
      local error_msg="${2:-El archivo '$file' no existe}"

      debug "Verificando archivo: $file"

      if [ ! -f "$file" ]; then
          error "$error_msg"
          return 1
      fi

      return 0
  }

params_schema:
  {"params":[{"name":"cmd","desc":"nombre del comando a verificar en PATH"},{"name":"error_code","desc":"codigo identificador del error; default COMMAND_NOT_FOUND"},{"name":"description","desc":"mensaje de error personalizado; default 'El comando CMD no esta disponible'"},{"name":"dir","desc":"ruta del directorio a verificar"},{"name":"file","desc":"ruta del archivo a verificar"}],"output":"exit code 0 si todas las verificaciones pasan; exit code 1 en caso de fallo con mensaje de error formateado"}


id: bash_colors_bash_shell
name: bash_colors
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: pure
signature: bash_colors() -> void
tags: ["bash","colors","ansi","terminal","symbols","box"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/bash_colors.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 8e8edd6620894fa6901ecb0882b74fadc6aca6a92080c228a5221dcbc450cc20
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
description:
  Exporta variables ANSI de colores, caracteres box drawing y simbolos unicode para uso en scripts de terminal.

example:
  ```bash
  source bash/functions/shell/bash_colors.sh
  bash_colors

  echo -e "${GREEN}Todo bien${NC}"
  echo -e "${RED}${CROSS} Error detectado${NC}"
  echo -e "${BOX_TL}${BOX_H}${BOX_TR}"
  ```

notes:
  Funcion pura: solo define y exporta variables de entorno, sin I/O ni efectos secundarios. Debe llamarse una vez antes de usar cualquier variable de color o simbolo.

  Variables de color disponibles: PURPLE, MAGENTA, GREEN, BLUE, YELLOW, RED, CYAN, ORANGE, GRAY, DIM_GRAY, BOLD, DIM, NC.

  Variables box drawing: BOX_TL, BOX_TR, BOX_BL, BOX_BR, BOX_H, BOX_V, BOX_ML, BOX_MR, BOX_SEP.

  Simbolos unicode: CHECKMARK, CROSS, ARROW, BULLET, WARNING, INFO.

code:
  # bash_colors
  # -----------
  # Variables ANSI de color, caracteres box drawing y simbolos para scripts.
  # Diseñado para ser sourced por otros scripts.
  #
  # USO (sourced):
  #   source bash_colors.sh
  #   bash_colors

  bash_colors() {
      export PURPLE='\033[35m'
      export MAGENTA='\033[35m'
      export GREEN='\033[0;32m'
      export BLUE='\033[0;34m'
      export YELLOW='\033[1;33m'
      export RED='\033[0;31m'
      export CYAN='\033[0;36m'
      export ORANGE='\033[0;33m'
      export GRAY='\033[0;90m'
      export DIM_GRAY='\033[2;37m'
      export BOLD='\033[1m'
      export DIM='\033[2m'
      export NC='\033[0m'

      export BOX_TL="╔"
      export BOX_TR="╗"
      export BOX_BL="╚"
      export BOX_BR="╝"
      export BOX_H="═"
      export BOX_V="║"
      export BOX_ML="╠"
      export BOX_MR="╣"
      export BOX_SEP="─"

      export CHECKMARK="✓"
      export CROSS="✗"
      export ARROW="→"
      export BULLET="•"
      export WARNING="⚠"
      export INFO="ℹ"
  }

params_schema:
  {"output":"exporta variables de entorno con codigos ANSI: colores (RED, GREEN, BLUE, etc.), box drawing (BOX_TL, BOX_H, etc.) y simbolos (CHECKMARK, CROSS, ARROW, etc.)"}


id: bash_confirm_bash_shell
name: bash_confirm
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: bash_confirm(prompt: string, [default: string]) -> exit_code
tags: ["bash","confirm","prompt","interactive","dialog"]
uses_functions: ["bash_colors_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_confirm.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 14857288f086d38b417c0d8f0861af195b2320dfe788927f36336027f6bf8694
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
description:
  Dialogo interactivo de confirmacion y/n con valor por defecto configurable. Soporta respuestas yes/y/si.

example:
  ```bash
  source bash/functions/shell/bash_confirm.sh

  # Con default no (muestra [y/N])
  if bash_confirm "¿Deseas continuar?"; then
      echo "Continuando..."
  fi

  # Con default yes (muestra [Y/n])
  bash_confirm "¿Eliminar archivos temporales?" "y" && rm -rf /tmp/cache
  ```

notes:
  El prompt muestra el default en mayuscula: `[Y/n]` cuando default=y, `[y/N]` cuando default=n. Si el usuario presiona Enter sin escribir, se usa el valor por defecto.

  Acepta variantes: y, Y, yes, YES, si, SI como afirmativo. Cualquier otra respuesta se trata como negativo.

  Usa colores de `bash_colors` para el prompt en amarillo. Requiere terminal interactiva (lee de stdin con `read -r`).

code:
  # bash_confirm
  # ------------
  # Dialogo de confirmacion y/n con valor por defecto.
  #
  # USO (sourced):
  #   source bash_confirm.sh
  #   bash_confirm "Continuar?" && echo "Si" || echo "No"
  #   bash_confirm "Eliminar?" "y"  # default yes

  SCRIPT_DIR_BASH_CONFIRM="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_CONFIRM/bash_colors.sh"
  bash_colors

  bash_confirm() {
      local prompt="${1:-¿Continuar?}"
      local default="${2:-n}"

      if [ "$default" = "y" ]; then
          prompt="$prompt [Y/n]"
      else
          prompt="$prompt [y/N]"
      fi

      echo -ne "${YELLOW}$prompt ${NC}"
      read -r response

      response=${response:-$default}
      case "$response" in
          [yY][eE][sS]|[yY]|[sS][iI])
              return 0
              ;;
          *)
              return 1
              ;;
      esac
  }

params_schema:
  {"params":[{"name":"prompt","desc":"pregunta a mostrar al usuario; default '¿Continuar?'"},{"name":"default","desc":"valor por defecto cuando el usuario presiona Enter sin escribir nada; 'y' o 'n'; default 'n'"}],"output":"exit code 0 si el usuario confirma (y/yes/si), exit code 1 si niega o acepta el default negativo"}


id: bash_handle_error_bash_shell
name: bash_handle_error
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: handle_error(error_code: string, error_description: string, [solution: string]) -> exit_code
tags: ["bash","error","handler","box","formatted","context"]
uses_functions: ["bash_colors_bash_shell","bash_log_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_handle_error.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: c94f84e7cc80dcbab8bad556a15013d997365a09c89fe2c8be159d0815d4bcbc
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
description:
  Muestra un box de error formateado con contexto del fallo: script, linea, funcion, directorio y usuario. Registra en log.

example:
  ```bash
  source bash/functions/shell/bash_handle_error.sh

  build_project() {
      go build ./... || handle_error "BUILD_FAILED" \
          "La compilacion del proyecto fallo" \
          "Ejecuta 'go mod tidy' y verifica que todas las dependencias esten instaladas"
  }

  connect_db() {
      psql "$DB_URL" -c '\q' 2>/dev/null || handle_error "DB_CONNECTION_ERROR" \
          "No se pudo conectar a la base de datos"
  }
  ```

notes:
  El box usa caracteres unicode de `bash_colors` (BOX_TL, BOX_H, etc.) para el borde en rojo. La informacion de contexto se extrae de `BASH_SOURCE`, `BASH_LINENO` y `FUNCNAME` con offset 2 para apuntar al caller del caller.

  La funcion siempre retorna 1, permitiendo usarla como `cmd || handle_error ...` en pipelines de error.

  Usa `bash_colors` y `bash_log` como dependencias. Sourcea ambas automaticamente al cargarse.

code:
  # bash_handle_error
  # -----------------
  # Muestra un box de error formateado con contexto del fallo.
  # Incluye script, linea, funcion y directorio.
  #
  # USO (sourced):
  #   source bash_handle_error.sh
  #   handle_error "BUILD_FAILED" "La compilacion fallo" "Verifica las dependencias"

  SCRIPT_DIR_BASH_HANDLE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_HANDLE/bash_colors.sh"
  source "$SCRIPT_DIR_BASH_HANDLE/bash_log.sh"
  bash_colors
  bash_log_init

  handle_error() {
      local error_code="$1"
      local error_description="$2"
      local solution="$3"

      echo ""
      echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
      echo -e "${RED}║                    ${CROSS} ERROR DETECTADO                    ║${NC}"
      echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
      echo ""
      echo -e "${RED}${BOLD}Error:${NC} ${error_description}"
      echo -e "${GRAY}Codigo: ${error_code}${NC}"
      echo ""

      if [ -n "$solution" ]; then
          echo -e "${YELLOW}${INFO} Solucion sugerida:${NC}"
          echo -e "   ${solution}"
          echo ""
      fi

      echo -e "${GRAY}${INFO} Informacion adicional:${NC}"
      echo -e "   ${GRAY}${BULLET} Script: ${BASH_SOURCE[2]:-desconocido}${NC}"
      echo -e "   ${GRAY}${BULLET} Linea: ${BASH_LINENO[1]:-desconocido}${NC}"
      echo -e "   ${GRAY}${BULLET} Funcion: ${FUNCNAME[2]:-main}${NC}"
      echo -e "   ${GRAY}${BULLET} Directorio: $(pwd)${NC}"
      echo -e "   ${GRAY}${BULLET} Usuario: $(whoami)${NC}"
      echo ""

      log "ERROR" "Code: $error_code | Description: $error_description | Script: ${BASH_SOURCE[2]} | Line: ${BASH_LINENO[1]}"

      return 1
  }

params_schema:
  {"params":[{"name":"error_code","desc":"codigo identificador del error, ej: BUILD_FAILED, DB_CONNECTION_ERROR"},{"name":"error_description","desc":"descripcion legible del error para mostrar al usuario"},{"name":"solution","desc":"solucion sugerida; opcional; si se provee se muestra en seccion separada"}],"output":"box de error formateado en stdout con contexto (script, linea, funcion, directorio, usuario) + registro en log; retorna exit code 1"}


id: bash_log_bash_shell
name: bash_log
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
tags: ["bash","log","logging","colors","terminal"]
uses_functions: ["bash_colors_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_log.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: acdf449809f9c33380c6914dbe9995bf1a7613bc800a4e5687c238634cf4e939
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
signature:
  bash_log_init() -> void; success(msg: string) -> void; info(msg: string) -> void; warning(msg: string) -> void; error(msg: string) -> void; debug(msg: string) -> void; progress(msg: string) -> void

description:
  Funciones de logging con colores para scripts bash. Incluye niveles success/info/warning/error/debug/progress con escritura a archivo de log.

example:
  ```bash
  source bash/functions/shell/bash_log.sh
  bash_log_init

  success "Servicio iniciado correctamente"
  info "Procesando 42 registros..."
  warning "El puerto 8080 ya esta en uso"
  error "No se pudo conectar a la base de datos"
  debug "Valor de variable: $VAR"
  progress "Descargando imagen Docker..."
  ```

notes:
  Llama a `bash_log_init` una vez al inicio para configurar ERROR_LOG_FILE y DEBUG_MODE. Por defecto el log va a `/tmp/script-errors-YYYYMMDD.log`.

  La variable de entorno `DEBUG_MODE=1` activa los mensajes de debug a stderr y muestra timestamps en todos los logs.

  `error` escribe a stderr; el resto a stdout. Todos los niveles escriben al archivo de log independientemente de DEBUG_MODE.

  Fuente: sourcea `bash_colors.sh` automaticamente al cargarse.

code:
  # bash_log
  # --------
  # Funciones de logging con colores para scripts.
  # Incluye: log, success, info, warning, error, debug, progress.
  #
  # USO (sourced):
  #   source bash_log.sh
  #   bash_log_init
  #   success "Operacion completada"
  #   info "Procesando..."
  #   error "Algo fallo"

  SCRIPT_DIR_BASH_LOG="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_LOG/bash_colors.sh"
  bash_colors

  bash_log_init() {
      export ERROR_LOG_FILE="${ERROR_LOG_FILE:-/tmp/script-errors-$(date +%Y%m%d).log}"
      export DEBUG_MODE="${DEBUG_MODE:-0}"
  }

  log() {
      local level="$1"
      shift
      local message="$@"
      local timestamp
      timestamp=$(date '+%Y-%m-%d %H:%M:%S')
      echo "[$timestamp] [$level] $message" >> "${ERROR_LOG_FILE:-/tmp/script-errors.log}"

      if [ "${DEBUG_MODE:-0}" = "1" ]; then
          echo -e "${GRAY}[$timestamp] [$level] $message${NC}" >&2
      fi
  }

  success() {
      echo -e "${GREEN}${CHECKMARK} $*${NC}"
      log "SUCCESS" "$*"
  }

  info() {
      echo -e "${BLUE}${INFO} $*${NC}"
      log "INFO" "$*"
  }

  warning() {
      echo -e "${YELLOW}${WARNING} $*${NC}"
      log "WARNING" "$*"
  }

  error() {
      echo -e "${RED}${CROSS} Error: $*${NC}" >&2
      log "ERROR" "$*"
  }

  debug() {
      if [ "${DEBUG_MODE:-0}" = "1" ]; then
          echo -e "${GRAY}[DEBUG] $*${NC}" >&2
      fi
      log "DEBUG" "$*"
  }

  progress() {
      echo -e "${CYAN}${ARROW} $*${NC}"
  }

params_schema:
  {"params":[{"name":"msg","desc":"mensaje a mostrar y registrar en el archivo de log"}],"output":"mensaje formateado con colores en stdout/stderr y registro con timestamp en archivo de log (ERROR_LOG_FILE)"}


id: bash_safe_run_bash_shell
name: bash_safe_run
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
tags: ["bash","safe","run","error","trap","handler"]
uses_functions: ["bash_log_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_safe_run.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: c9efa39908e0dde3bd6695452def89c00a56d1cb84febd0cfd6650207276248b
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
signature:
  safe_run(cmd: string, [error_code: string], [error_desc: string]) -> void; setup_error_trap() -> void; error_trap_handler(exit_code: int, line_number: int) -> void

description:
  Ejecuta comandos con manejo de errores integrado. Incluye trap handler que captura fallos con numero de linea y codigo de salida.

example:
  ```bash
  source bash/functions/shell/bash_safe_run.sh
  bash_log_init
  setup_error_trap

  safe_run "go build ./..." "BUILD_FAILED" "La compilacion fallo"
  safe_run "docker compose up -d" "DOCKER_FAILED" "No se pudo iniciar Docker Compose"
  safe_run "npm install" "NPM_FAILED"
  ```

notes:
  `safe_run` usa `eval` internamente para ejecutar el comando, lo que permite pasar comandos con pipes y redirecciones como string. Usar con precaucion en entornos con input no confiable.

  `setup_error_trap` instala un trap `ERR` que llama a `error_trap_handler` automaticamente en cualquier comando fallido del script, mostrando numero de linea y codigo de salida.

  `error_trap_handler` no llama a `exit` — el caller decide si continuar o abortar. Muestra la ruta al log para debugging.

code:
  # bash_safe_run
  # -------------
  # Ejecutar comandos con manejo de errores y trap.
  # Incluye safe_run, setup_error_trap y error_trap_handler.
  #
  # USO (sourced):
  #   source bash_safe_run.sh
  #   setup_error_trap
  #   safe_run "go build ./..." "BUILD_FAILED" "La compilacion fallo"

  SCRIPT_DIR_BASH_SAFE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_SAFE/bash_log.sh"
  bash_log_init

  safe_run() {
      local cmd="$1"
      local error_code="${2:-COMMAND_FAILED}"
      local error_desc="${3:-El comando fallo: $cmd}"

      debug "Ejecutando: $cmd"

      if ! eval "$cmd"; then
          error "$error_desc ($error_code)"
          return 1
      fi

      return 0
  }

  setup_error_trap() {
      trap 'error_trap_handler $? $LINENO' ERR
  }

  error_trap_handler() {
      local exit_code=$1
      local line_number=$2

      if [ "$exit_code" -ne 0 ]; then
          echo ""
          error "El script fallo en la linea $line_number con codigo de salida $exit_code"
          echo -e "${GRAY}Consulta el log: ${ERROR_LOG_FILE:-/tmp/script-errors.log}${NC}"
          echo ""
      fi
  }

params_schema:
  {"params":[{"name":"cmd","desc":"comando a ejecutar via eval"},{"name":"error_code","desc":"codigo identificador del error en caso de fallo; default COMMAND_FAILED"},{"name":"error_desc","desc":"descripcion del error a mostrar; default 'El comando fallo: CMD'"}],"output":"exit code 0 si el comando tuvo exito; exit code 1 con mensaje de error formateado si fallo"}


id: convert_text_case_bash_shell
name: convert_text_case
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: convert_text_case(mode: string, file: string, [output_file: string]) -> void
tags: ["bash","text","convert","case","encoding"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/convert_text_case.sh
created_at: 2026-04-08T21:29:50Z
updated_at: 2026-04-08T21:29:50Z
props: []
emits: []
variant: []
content_hash: 1df7df6d453898ff7a9158dbb44c977a20a990f1ef515158febfb2b1a60d9c0f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/conversores/conversor_texto.sh
description:
  Convierte el contenido de un archivo de texto. Modos: upper (todo mayúsculas), lower (todo minúsculas), lf (normaliza saltos de línea a LF eliminando \r), crlf (normaliza saltos a CRLF). Sin output_file imprime a stdout.

example:
  ```bash
  source bash/functions/shell/convert_text_case.sh

  # Convertir a mayúsculas y mostrar en stdout
  convert_text_case upper mi_archivo.txt

  # Convertir a minúsculas y guardar en archivo
  convert_text_case lower input.txt output_lower.txt

  # Normalizar saltos de línea CRLF a LF
  convert_text_case lf archivo_windows.txt archivo_unix.txt

  # Añadir CRLF (para Windows)
  convert_text_case crlf unix.txt windows.txt
  ```

notes:
  Usa `awk` para las conversiones de case (portable) y `sed` para los saltos de línea. La función no modifica el archivo original si se provee `output_file`. Sin `output_file` imprime directamente a stdout, útil para pipes.

code:
  #!/usr/bin/env bash
  # convert_text_case
  # -----------------
  # Convierte el contenido de texto de un archivo aplicando transformaciones:
  # upper (mayúsculas), lower (minúsculas), lf (normalizar saltos a LF),
  # crlf (normalizar saltos a CRLF).
  #
  # USO:
  #   source convert_text_case.sh
  #   convert_text_case mode file [output_file]
  #
  # ARGUMENTOS:
  #   mode         Transformación a aplicar: upper|lower|lf|crlf (requerido)
  #   file         Archivo de entrada (requerido)
  #   output_file  Archivo de salida (opcional; por defecto imprime a stdout)

  convert_text_case() {
      local mode="${1:-}"
      local input_file="${2:-}"
      local output_file="${3:-}"

      # Validar argumentos requeridos
      if [[ -z "$mode" ]]; then
          echo "convert_text_case: modo requerido (upper|lower|lf|crlf)" >&2
          return 1
      fi

      if [[ -z "$input_file" ]]; then
          echo "convert_text_case: archivo de entrada requerido" >&2
          return 1
      fi

      if [[ ! -f "$input_file" ]]; then
          echo "convert_text_case: archivo no encontrado: ${input_file}" >&2
          return 1
      fi

      # Función auxiliar que aplica la conversión y escribe a stdout o archivo
      _apply_conversion() {
          local src="$1"
          local op="$2"
          case "$op" in
              upper)
                  awk '{ print toupper($0) }' "$src"
                  ;;
              lower)
                  awk '{ print tolower($0) }' "$src"
                  ;;
              lf)
                  sed 's/\r$//' "$src"
                  ;;
              crlf)
                  sed 's/\r$//' "$src" | sed 's/$/\r/'
                  ;;
              *)
                  echo "convert_text_case: modo no soportado: ${op}. Usa: upper|lower|lf|crlf" >&2
                  return 1
                  ;;
          esac
      }

      if [[ -n "$output_file" ]]; then
          _apply_conversion "$input_file" "$mode" > "$output_file"
          echo "convert_text_case: ${input_file} → ${output_file} (modo: ${mode})"
      else
          _apply_conversion "$input_file" "$mode"
      fi
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      convert_text_case "$@"
  fi

params_schema:
  {"params":[{"name":"mode","desc":"transformación a aplicar: upper|lower|lf|crlf (requerido)"},{"name":"file","desc":"ruta al archivo de entrada (requerido)"},{"name":"output_file","desc":"ruta al archivo de salida (opcional; default: stdout)"}],"output":"texto convertido a stdout o escrito en output_file; exit code 1 si el modo o archivo son inválidos"}


id: create_project_structure_bash_shell
name: create_project_structure
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: create_project_structure(project_name: string) -> void
tags: ["bash","project","structure","scaffold","init"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/create_project_structure.sh
created_at: 2026-04-08T21:29:50Z
updated_at: 2026-04-08T21:29:50Z
props: []
emits: []
variant: []
content_hash: 7ecd70311b4e366a9ce1c21832739ab6b1a6cca35aa30576e8510b7f57b64c25
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/inicializar_repos/functional_structure.sh
description:
  Crea la estructura de directorios estándar de un proyecto funcional en el directorio indicado: database (attachments, data, models), dist (desktop/mobile/docker), docker, docs, frontend, logs, notebooks, robots, scripts, src (application/core/middleware con tests y tipos).

example:
  ```bash
  source bash/functions/shell/create_project_structure.sh

  # Crear estructura en un directorio nuevo
  create_project_structure mi-proyecto

  # Usar una ruta
  create_project_structure /home/user/projects/nuevo-proyecto
  ```

notes:
  Crea 68 directorios organizados en la estructura funcional de DevLauncher. La estructura separa claramente application (orquestación), core (lógica pura) y middleware (efectos/I/O) bajo `src/`. No crea archivos, solo directorios. Idempotente: si los directorios ya existen, no falla.

code:
  #!/usr/bin/env bash
  # create_project_structure
  # ------------------------
  # Crea la estructura de directorios estándar de un proyecto funcional
  # (database, dist, docker, docs, frontend, logs, notebooks, robots,
  # scripts, src con application/core/middleware) en el directorio indicado.
  #
  # USO:
  #   source create_project_structure.sh
  #   create_project_structure project_name
  #
  # ARGUMENTOS:
  #   project_name   Nombre del proyecto / ruta destino (requerido)

  create_project_structure() {
      local project_name="${1:-}"

      if [[ -z "$project_name" ]]; then
          echo "create_project_structure: se requiere el nombre del proyecto" >&2
          echo "  Uso: create_project_structure <project_name>" >&2
          return 1
      fi

      local destino="$project_name"

      local directories=(
          "database"
          "database/attachments"
          "database/attachments/audio"
          "database/attachments/docs"
          "database/attachments/images"
          "database/attachments/video"
          "database/data"
          "database/data/01_raw"
          "database/data/02_clean"
          "database/data/03_objects"
          "database/data/04_answers"
          "database/models"
          "dist"
          "dist/desktop"
          "dist/desktop/linux"
          "dist/desktop/mac"
          "dist/desktop/win"
          "dist/docker"
          "dist/mobile"
          "dist/mobile/android"
          "dist/mobile/ios"
          "docker"
          "docs"
          "docs/documentation"
          "docs/pdf"
          "frontend"
          "logs"
          "notebooks"
          "robots"
          "scripts"
          "src"
          "src/application"
          "src/application/event"
          "src/application/jobs"
          "src/application/pipes"
          "src/application/sandbox"
          "src/application/simulation"
          "src/application/tasks"
          "src/application/tests"
          "src/application/tests/benchmark"
          "src/application/tests/fuzzers"
          "src/application/tests/mocks"
          "src/application/tests/mutation"
          "src/core"
          "src/core/base"
          "src/core/functions"
          "src/core/rules"
          "src/core/tests"
          "src/core/tests/benchmark"
          "src/core/tests/fuzzers"
          "src/core/tests/mocks"
          "src/core/tests/mutation"
          "src/core/types"
          "src/core/utils"
          "src/middleware"
          "src/middleware/api"
          "src/middleware/backend"
          "src/middleware/browser"
          "src/middleware/config"
          "src/middleware/connection"
          "src/middleware/controller"
          "src/middleware/i18n"
          "src/middleware/interfaces"
          "src/middleware/queue"
          "src/middleware/servers"
          "src/middleware/tests"
          "src/middleware/tests/benchmark"
          "src/middleware/tests/fuzzers"
          "src/middleware/tests/mocks"
          "src/middleware/tests/mutation"
          "src/middleware/types"
          "src/modules"
      )

      echo "Creando estructura del proyecto: ${destino}"

      for dir in "${directories[@]}"; do
          mkdir -p "${destino%/}/${dir}"
      done

      echo "Estructura creada en: ${destino%/}"
      echo "  ${#directories[@]} directorios creados."
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      create_project_structure "$@"
  fi

params_schema:
  {"params":[{"name":"project_name","desc":"nombre del proyecto o ruta destino donde crear la estructura (requerido)"}],"output":"crea los directorios a stdout con conteo final; exit code 1 si no se proporciona nombre"}


id: exit_with_status_bash_shell
name: exit_with_status
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: pure
signature: exit_with_status(total_steps: int, ok_steps: int, failed_steps: int) -> int
tags: ["execution","status","exit-code","standard"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/exit_with_status.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3c6882702f826c950f90bf52e72130faf89681f0866e65e8653312e779a3075e
description:
  Calcula el exit code estandar (0=success, 1=failure, 2=partial) a partir de contadores de pasos. Si failed_steps=0 imprime 0 y sale con 0. Si ok_steps=0 imprime 1 y sale con 1. Si hay ambos imprime 2 y sale con 2.

example:
  ```bash
  source bash/functions/shell/exit_with_status.sh

  exit_with_status 5 5 0  # stdout: 0, exit code: 0
  exit_with_status 5 0 5  # stdout: 1, exit code: 1
  exit_with_status 5 3 2  # stdout: 2, exit code: 2
  ```

notes:
  Funcion pura: no realiza I/O de sistema, no modifica estado global, no lee variables de entorno. El argumento `total_steps` se recibe para completitud semantica pero la logica solo depende de `ok_steps` y `failed_steps`. El valor se imprime a stdout ademas de usarse como exit code, de modo que el caller puede capturarlo con `$(exit_with_status ...)` o evaluar directamente con `exit_with_status ... ; echo $?`.

code:
  #!/usr/bin/env bash
  # exit_with_status — calcula el exit code estandar a partir de contadores de pasos

  # exit_with_status <total_steps> <ok_steps> <failed_steps>
  #
  # Calcula el exit code estandar:
  #   0 — todos los pasos exitosos (failed_steps == 0)
  #   1 — todos los pasos fallaron (ok_steps == 0)
  #   2 — resultado parcial (hay ok y failed)
  #
  # Imprime el codigo a stdout y sale con ese codigo.
  exit_with_status() {
      local total_steps="$1"
      local ok_steps="$2"
      local failed_steps="$3"

      local code

      if [[ "$failed_steps" -eq 0 ]]; then
          code=0
      elif [[ "$ok_steps" -eq 0 ]]; then
          code=1
      else
          code=2
      fi

      echo "$code"
      return "$code"
  }

params_schema:
  {"params":[{"name":"total_steps","desc":"número total de pasos ejecutados"},{"name":"ok_steps","desc":"número de pasos exitosos"},{"name":"failed_steps","desc":"número de pasos fallidos"}],"output":"exit code: 0 (éxito), 1 (fallo), o 2 (parcial)"}


id: find_free_port_bash_shell
name: find_free_port
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: find_free_port([start_port: int], [end_port: int]) -> int
tags: ["network","port","shell","utility"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/find_free_port.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b89bacb32e1a6293e15b43b58a72c92587d3c5b30e1ac95cda12356948561e82
description:
  Busca el primer puerto TCP libre en un rango dado usando ss y lsof. Retorna el numero de puerto a stdout.

example:
  ```bash
  source find_free_port.sh
  port=$(find_free_port 8888 8899)
  echo "Puerto libre: $port"

  # Con defaults (8888-8899)
  port=$(find_free_port)
  ```

notes:
  Usa `ss -tln` como primer intento y `lsof` como fallback. Ambos deben confirmar que el puerto no esta en uso. Si ningun puerto esta libre en el rango, sale con exit code 1.

code:
  # find_free_port
  # ---------------
  # Busca el primer puerto libre en un rango dado.
  # Imprime el puerto encontrado a stdout.
  # Sale con exit code 1 si no encuentra ninguno.
  #
  # USO (sourced):
  #   source find_free_port.sh
  #   port=$(find_free_port 8888 8899)

  find_free_port() {
      local start="${1:-8888}"
      local end="${2:-8899}"

      for ((port=start; port<=end; port++)); do
          if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
             ! lsof -i:"$port" >/dev/null 2>&1; then
              echo "$port"
              return 0
          fi
      done

      echo "find_free_port: no se encontro puerto libre en rango ${start}-${end}" >&2
      return 1
  }

params_schema:
  {"params":[{"name":"start_port","desc":"puerto inicial del rango (default: 8888)"},{"name":"end_port","desc":"puerto final del rango (default: 8899)"}],"output":"número de puerto TCP libre"}


id: git_clean_branches_bash_shell
name: git_clean_branches
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_clean_branches([base_branch: string], [--remote], [--force]) -> void
tags: ["bash","git","branches","clean","merge"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_clean_branches.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: e15fdf0e7a18d3c6349f8fa942ce387b0f8319fa6390fc9b32e7b6d6e9a4bcc5
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/limpiar_ramas.sh
description:
  Elimina ramas locales ya mergeadas en la rama base (autodetecta main/master). Con --remote también borra las ramas en todos los remotes. Con --force omite la confirmación interactiva.

example:
  ```bash
  source bash/functions/shell/git_clean_branches.sh

  # Limpieza básica con confirmación
  git_clean_branches

  # Sin confirmación, sobre rama base "develop"
  git_clean_branches develop --force

  # Eliminar también en remotes
  git_clean_branches main --remote --force
  ```

notes:
  Ramas protegidas que nunca se eliminan: `main`, `master`, `develop`, `dev`, `staging`, `release`. Siempre hace prune de referencias remotas obsoletas antes de buscar candidatos.

code:
  #!/usr/bin/env bash
  # git_clean_branches
  # ------------------
  # Elimina ramas locales ya mergeadas en la rama base (main/master por defecto).
  # Opcionalmente también elimina las ramas en remotes.
  #
  # USO:
  #   source git_clean_branches.sh
  #   git_clean_branches [base_branch] [--remote] [--force]
  #
  # ARGUMENTOS:
  #   base_branch  Rama base (opcional; autodetecta main/master si se omite)
  #   --remote     Elimina también las ramas en todos los remotes
  #   --force      Omite confirmación interactiva

  git_clean_branches() {
      local base_branch=""
      local delete_remote=false
      local force=false

      # Parsear argumentos
      for arg in "$@"; do
          case "$arg" in
              --remote) delete_remote=true ;;
              --force)  force=true ;;
              *)        [[ -z "$base_branch" ]] && base_branch="$arg" ;;
          esac
      done

      # Validar repo
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_clean_branches: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      # Detectar rama base
      if [[ -z "$base_branch" ]]; then
          if git show-ref --verify --quiet refs/heads/main; then
              base_branch="main"
          elif git show-ref --verify --quiet refs/heads/master; then
              base_branch="master"
          else
              echo "git_clean_branches: no se detectó main/master; pasa la rama base como argumento" >&2
              return 1
          fi
      fi

      echo "git_clean_branches: rama base = ${base_branch}"

      # Hacer prune de referencias remotas obsoletas
      echo "Haciendo prune de referencias remotas..."
      git remote | while IFS= read -r remote; do
          git remote prune "$remote" 2>/dev/null && echo "  Pruned: ${remote}"
      done
      echo ""

      # Buscar ramas locales mergeadas
      local candidates
      candidates="$(git branch --merged "$base_branch" \
          | grep -v -E "^\*|^\s*(main|master|develop|dev|staging|release)$" \
          | sed 's/^[[:space:]]*//' \
          | grep -v "^$" || true)"

      if [[ -z "$candidates" ]]; then
          echo "No hay ramas locales mergeadas para eliminar."
          return 0
      fi

      echo "Ramas candidatas a eliminar:"
      echo "$candidates" | while IFS= read -r b; do
          echo "  - ${b}"
      done
      echo ""

      # Confirmación (omitir si --force)
      if [[ "$force" != true ]]; then
          echo -n "¿Eliminar estas ramas locales? [y/N] "
          read -r answer
          [[ "$answer" != "y" && "$answer" != "Y" ]] && { echo "Operación cancelada."; return 0; }
      fi

      # Eliminar ramas locales
      local deleted=0
      local failed=0
      while IFS= read -r branch; do
          if git branch -d "$branch"; then
              echo "  Eliminada local: ${branch}"
              deleted=$((deleted + 1))
          else
              echo "  No se pudo eliminar: ${branch}" >&2
              failed=$((failed + 1))
          fi
      done <<< "$candidates"

      echo ""
      echo "Resumen: ${deleted} eliminada(s), ${failed} fallida(s)"

      # Eliminar en remotes si se solicitó
      if [[ "$delete_remote" == true ]]; then
          local remotes
          remotes="$(git remote 2>/dev/null || true)"
          if [[ -n "$remotes" ]]; then
              echo ""
              echo "Eliminando ramas en remotes..."
              while IFS= read -r branch; do
                  while IFS= read -r remote; do
                      if git ls-remote --heads "$remote" "$branch" 2>/dev/null | grep -q "$branch"; then
                          if git push "$remote" --delete "$branch"; then
                              echo "  Eliminada remota: ${remote}/${branch}"
                          else
                              echo "  No se pudo eliminar remota: ${remote}/${branch}" >&2
                          fi
                      fi
                  done <<< "$remotes"
              done <<< "$candidates"
          fi
      fi

      echo ""
      echo "Limpieza completada."
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_clean_branches "$@"
  fi

params_schema:
  {"params":[{"name":"base_branch","desc":"rama base para detectar merges (opcional; autodetecta main o master)"},{"name":"--remote","desc":"flag para también eliminar ramas en todos los remotes"},{"name":"--force","desc":"flag para omitir confirmación interactiva"}],"output":"lista de ramas eliminadas a stdout; exit code 1 si no es repo Git o no se detecta la rama base"}


id: git_log_visual_bash_shell
name: git_log_visual
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_log_visual([--count N: int], [--author X: string], [--since D: string], [--all]) -> void
tags: ["bash","git","log","history","graph"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_log_visual.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 9390c00f79081d7797e1931ddc82b3101a80ebf260dec78265ba92aed2ffc70f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/historial_commits.sh
description:
  Muestra el historial de commits Git con grafo visual, colores y formato legible (hash, fecha, autor, mensaje, ramas). Soporta filtros por cantidad, autor, fecha y modo todas-las-ramas. Pagina con less.

example:
  ```bash
  source bash/functions/shell/git_log_visual.sh

  # Últimos 20 commits (default)
  git_log_visual

  # Últimos 50 commits de todas las ramas
  git_log_visual --count 50 --all

  # Commits de un autor esta semana
  git_log_visual --author "lucas" --since "1 week ago"

  # Commits de hoy en todas las ramas
  git_log_visual --since "00:00:00" --all --count 100
  ```

notes:
  Usa `less -R --quit-if-one-screen` para paginar: si el log cabe en pantalla, no abre el paginador. El formato incluye hash corto (amarillo), fecha (cyan), autor (verde), mensaje y refs de ramas (rojo).

code:
  #!/usr/bin/env bash
  # git_log_visual
  # --------------
  # Muestra el historial de commits con grafo visual, colores y formato legible.
  # Soporta filtros por cantidad, autor, fecha y todas las ramas.
  #
  # USO:
  #   source git_log_visual.sh
  #   git_log_visual [--count N] [--author X] [--since D] [--all]
  #
  # ARGUMENTOS:
  #   --count N    Número de commits a mostrar (default: 20)
  #   --author X   Filtrar por nombre o email del autor
  #   --since D    Mostrar commits desde fecha (ej: "1 week ago", "2025-01-01")
  #   --all        Incluir todas las ramas

  git_log_visual() {
      local count=20
      local author=""
      local since=""
      local all_branches=false

      # Parsear argumentos
      while [[ $# -gt 0 ]]; do
          case "$1" in
              --count|-n)
                  count="$2"
                  shift 2
                  ;;
              --author|-a)
                  author="$2"
                  shift 2
                  ;;
              --since|-s)
                  since="$2"
                  shift 2
                  ;;
              --all)
                  all_branches=true
                  shift
                  ;;
              *)
                  shift
                  ;;
          esac
      done

      # Validar repo
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_log_visual: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      local log_format="%C(yellow)%h%C(reset) %C(cyan)%ad%C(reset) %C(green)%an%C(reset) %s%C(red)%d%C(reset)"

      # Construir argumentos del comando
      local args=()
      args+=("--graph")
      args+=("--color=always")
      args+=("--date=short")
      args+=("--format=${log_format}")
      args+=("-n" "${count}")

      [[ -n "$author" ]] && args+=("--author=${author}")
      [[ -n "$since" ]]  && args+=("--since=${since}")
      [[ "$all_branches" == true ]] && args+=("--all")

      # Ejecutar
      git log "${args[@]}" 2>/dev/null | less -R --quit-if-one-screen
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_log_visual "$@"
  fi

params_schema:
  {"params":[{"name":"--count","desc":"número de commits a mostrar (default: 20)"},{"name":"--author","desc":"filtrar por nombre o email del autor (parcial, igual que git --author)"},{"name":"--since","desc":"mostrar solo commits desde esta fecha (ej: '1 week ago', '2025-01-01')"},{"name":"--all","desc":"incluir commits de todas las ramas, no solo la actual"}],"output":"historial de commits paginado con less -R; exit code 1 si no es un repo Git"}


id: git_push_all_remotes_bash_shell
name: git_push_all_remotes
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_push_all_remotes([--message msg: string]) -> void
tags: ["bash","git","push","remote","all"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_push_all_remotes.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 1cbae6159ea94b009645630eb3376359421e0fb42d18331226914e8d0651f29e
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/push_todos_remotes.sh
description:
  Hace commit de todos los cambios pendientes (git add -A + git commit) y pushea la rama actual a todos los remotes configurados. Si no hay cambios solo pushea. Requiere --message si hay cambios sin commitear.

example:
  ```bash
  source bash/functions/shell/git_push_all_remotes.sh

  # Solo push (sin cambios pendientes)
  git_push_all_remotes

  # Commit + push a todos los remotes
  git_push_all_remotes --message "feat: nueva funcionalidad"
  ```

notes:
  Usa `--set-upstream` automáticamente si la rama no existe en el remote. Sale con exit code 1 si hay cambios pero no se pasa `--message`, o si algún push falla.

code:
  #!/usr/bin/env bash
  # git_push_all_remotes
  # --------------------
  # Hace commit de todos los cambios pendientes (si los hay) y pushea
  # a todos los remotes configurados en el repositorio.
  #
  # USO:
  #   source git_push_all_remotes.sh
  #   git_push_all_remotes [--message "mensaje"]
  #
  # ARGUMENTOS:
  #   --message "msg"  Mensaje de commit (si hay cambios). Si se omite y hay
  #                    cambios, sale con error.

  git_push_all_remotes() {
      local commit_msg=""

      # Parsear argumentos
      while [[ $# -gt 0 ]]; do
          case "$1" in
              --message|-m)
                  commit_msg="$2"
                  shift 2
                  ;;
              *)
                  shift
                  ;;
          esac
      done

      # Validar repo
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_push_all_remotes: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      local branch
      branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"

      local remotes
      remotes="$(git remote 2>/dev/null || true)"

      if [[ -z "$remotes" ]]; then
          echo "git_push_all_remotes: no hay remotes configurados en este repositorio" >&2
          return 1
      fi

      echo "Rama actual:  ${branch}"
      echo "Remotes:"
      echo "$remotes" | while IFS= read -r r; do
          local url
          url="$(git remote get-url "$r" 2>/dev/null || echo "?")"
          echo "  ${r} → ${url}"
      done
      echo ""

      # Gestión del commit si hay cambios
      local has_changes=false
      if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
          has_changes=true
      fi

      if [[ "$has_changes" == true ]]; then
          if [[ -z "$commit_msg" ]]; then
              echo "git_push_all_remotes: hay cambios pero no se proporcionó --message" >&2
              echo "  Usa: git_push_all_remotes --message \"tu mensaje\"" >&2
              return 1
          fi

          echo "Cambios detectados:"
          git status --short | while IFS= read -r line; do
              echo "  ${line}"
          done
          echo ""

          echo "Añadiendo todos los cambios..."
          git add -A

          echo "Haciendo commit: \"${commit_msg}\""
          git commit -m "$commit_msg"
          echo ""
      else
          echo "Directorio de trabajo limpio. Se empujarán commits existentes."
          echo ""
      fi

      # Push a cada remote
      local ok_count=0
      local fail_count=0
      local failed_remotes=()
      local remote_count
      remote_count="$(echo "$remotes" | wc -l | tr -d ' ')"

      while IFS= read -r remote; do
          # Verificar si la rama existe en el remote
          local push_flags=""
          if ! git ls-remote --heads "$remote" "$branch" 2>/dev/null | grep -q "$branch"; then
              push_flags="--set-upstream"
              echo "La rama '${branch}' no existe en '${remote}', se creará"
          fi

          echo "Push → ${remote} (${branch})..."
          # shellcheck disable=SC2086
          if git push $push_flags "$remote" "$branch" 2>&1; then
              echo "  Push completado → ${remote}/${branch}"
              ok_count=$((ok_count + 1))
          else
              echo "  Falló el push a ${remote}/${branch}" >&2
              fail_count=$((fail_count + 1))
              failed_remotes+=("$remote")
          fi
          echo ""
      done <<< "$remotes"

      echo "=== Resumen ==="
      echo "Push completado en ${ok_count}/${remote_count} remote(s)"

      if [[ $fail_count -gt 0 ]]; then
          echo "Fallaron ${fail_count} remote(s): ${failed_remotes[*]}" >&2
          return 1
      fi
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_push_all_remotes "$@"
  fi

params_schema:
  {"params":[{"name":"--message","desc":"mensaje de commit a usar si hay cambios pendientes (requerido cuando hay cambios)"}],"output":"progreso del push a stdout por cada remote; exit code 1 si algún push falla"}


id: git_repo_status_bash_shell
name: git_repo_status
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_repo_status() -> void
tags: ["bash","git","status","repo","branch"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_repo_status.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 2ad1031d4d97162d28d9a2870366de255bf7c4a528d8a9bd5c18ddb25a60d0a1
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/estado_repo.sh
description:
  Muestra el estado completo de un repositorio Git: rama actual, upstream (ahead/behind), cambios pendientes, stash, remotes y últimos 8 commits. Sale con exit code 1 si el directorio actual no es un repo Git.

example:
  ```bash
  # Desde un directorio con repo git
  cd /home/user/my-project
  source bash/functions/shell/git_repo_status.sh
  git_repo_status
  ```

notes:
  No requiere dependencias externas más allá de git. Los colores del log de commits usan `--color=always` de git directamente. No produce output en stdout en caso de error — los mensajes de error van a stderr.

code:
  #!/usr/bin/env bash
  # git_repo_status
  # ---------------
  # Muestra el estado completo de un repositorio Git: rama actual, upstream,
  # cambios pendientes, stash, remotes y últimos commits.
  # Sale con exit code 1 si el directorio actual no es un repositorio Git.
  #
  # USO:
  #   source git_repo_status.sh
  #   git_repo_status
  #
  # O como script directo:
  #   bash git_repo_status.sh

  git_repo_status() {
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_repo_status: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      local branch
      branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"

      # Upstream
      local upstream
      upstream="$(git rev-parse --abbrev-ref "${branch}@{upstream}" 2>/dev/null || echo "")"
      local upstream_info
      if [[ -z "$upstream" ]]; then
          upstream_info="sin upstream"
      else
          local ahead behind
          ahead="$(git rev-list "${upstream}..HEAD" --count 2>/dev/null || echo 0)"
          behind="$(git rev-list "HEAD..${upstream}" --count 2>/dev/null || echo 0)"
          upstream_info="↑${ahead} ↓${behind} (${upstream})"
      fi

      echo "=== Rama & Upstream ==="
      echo "  Rama actual:  ${branch}"
      echo "  Upstream:     ${upstream_info}"
      echo ""

      # Cambios
      echo "=== Cambios ==="
      local changes
      changes="$(git status --short 2>/dev/null)"
      if [[ -z "$changes" ]]; then
          echo "  Directorio de trabajo limpio"
      else
          echo "  Cambios pendientes:"
          echo "$changes" | while IFS= read -r line; do
              echo "    ${line}"
          done
      fi
      echo ""

      # Stash
      echo "=== Stash ==="
      local stash_count
      stash_count="$(git stash list 2>/dev/null | wc -l | tr -d ' ')"
      if [[ "$stash_count" -eq 0 ]]; then
          echo "  Sin entradas en stash"
      else
          echo "  ${stash_count} entrada(s) en stash:"
          git stash list | head -5 | while IFS= read -r line; do
              echo "    ${line}"
          done
      fi
      echo ""

      # Remotes
      echo "=== Remotes ==="
      local remotes
      remotes="$(git remote -v 2>/dev/null | grep '(fetch)' || true)"
      if [[ -z "$remotes" ]]; then
          echo "  Sin remotes configurados"
      else
          echo "$remotes" | while IFS= read -r line; do
              local name url
              name="$(echo "$line" | awk '{print $1}')"
              url="$(echo "$line" | awk '{print $2}')"
              echo "  ${name} → ${url}"
          done
      fi
      echo ""

      # Últimos commits
      echo "=== Últimos commits ==="
      git log --oneline --decorate --color=always -8 2>/dev/null || true
      echo ""
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_repo_status "$@"
  fi

params_schema:
  {"params":[{"name":"(ninguno)","desc":"opera sobre el directorio de trabajo actual (cwd)"}],"output":"imprime el estado completo del repo a stdout; exit code 1 si no es un repo Git"}


id: report_execution_json_bash_shell
name: report_execution_json
kind: function
lang: bash
domain: shell
version: 2.0.0
purity: pure
tags: ["execution","json","report","standard","shell","pure"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/report_execution_json.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d069da827fcca273c1d63cc45ddb2f3fccd4240f176f870a45843381f87fa1be
signature:
  report_execution_json(flow_name: string, status: string, exit_code: int, started_at: string, ended_at: string, duration_ms: int, steps_file: string) -> string

description:
  Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry (docs/execution_standard.md). Recibe los metadatos del flujo y un archivo TSV con resultados de pasos (columnas: name, action, status, elapsed_ms, output, error). Imprime el JSON completo a stdout. Usa jq si esta disponible, con fallback a printf. Funcion pura: sin efectos secundarios ni I/O adicional.

example:
  ```bash
  source bash/functions/shell/report_execution_json.sh

  # Crear archivo de pasos (TSV sin cabecera, 6 columnas)
  cat > /tmp/steps.tsv <<'EOF'
  check_db	command	ok	10	exists
  backup	command	ok	450	done
  push	command	error	2540		remote rejected
  EOF

  report_execution_json \
      "backup_db" "partial" 2 \
      "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 \
      /tmp/steps.tsv
  ```

  Output esperado:

  ```json
  {"name":"backup_db","status":"partial","exit_code":2,"started_at":"2026-04-01T02:00:00Z","ended_at":"2026-04-01T02:00:03Z","duration_ms":3000,"steps_total":3,"steps_ok":2,"steps_failed":1,"steps":[{"name":"check_db","action":"command","status":"ok","elapsed_ms":10,"output":"exists"},{"name":"backup","action":"command","status":"ok","elapsed_ms":450,"output":"done"},{"name":"push","action":"command","status":"error","elapsed_ms":2540,"error":"remote rejected"}]}
  ```

notes:
  Funcion pura: solo lee el archivo de pasos y escribe JSON a stdout, sin efectos secundarios ni I/O adicional mas alla de leer el steps_file.

  Formato TSV: 6 columnas separadas por tabulador real (sin cabecera): name, action, status (ok|error), elapsed_ms, output, error. Los campos output y error pueden estar vacios; se omiten del JSON si no tienen valor, siguiendo el estandar de output estructurado.

  El caller es responsable de calcular status (success|failure|partial) y exit_code (0|1|2) segun las reglas del estandar. Ver exit_with_status_bash_shell para esa logica.

  Compatibilidad dual: con jq construye el JSON de forma robusta (maneja caracteres especiales y saltos de linea en output/error). Sin jq, el fallback con printf escapa backslash, comillas dobles y caracteres de control basicos.

  Puede ejecutarse directamente: `bash report_execution_json.sh <args>`.

code:
  #!/usr/bin/env bash
  # report_execution_json — Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry.
  #
  # USO (sourced):
  #   source report_execution_json.sh
  #   report_execution_json "backup_db" "partial" 2 \
  #       "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
  #
  # USO (ejecutado directamente):
  #   bash report_execution_json.sh "backup_db" "partial" 2 \
  #       "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
  #
  # FORMATO steps_file (TSV sin cabecera, 6 columnas):
  #   name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
  #   Los campos output y error pueden estar vacios.
  #   status valido: ok | error
  #
  # NOTA sobre IFS y tabs: bash trata tab como whitespace en IFS y colapsa
  # campos vacios consecutivos. Se usa 'cut -f N' para parsear cada columna
  # de forma correcta cuando hay campos vacios entre tabs.

  report_execution_json() {
      local flow_name="$1"
      local status="$2"
      local exit_code="$3"
      local started_at="$4"
      local ended_at="$5"
      local duration_ms="$6"
      local steps_file="$7"

      if [[ -z "$flow_name" || -z "$status" || -z "$exit_code" || \
            -z "$started_at" || -z "$ended_at" || -z "$duration_ms" || \
            -z "$steps_file" ]]; then
          echo "report_execution_json: uso: report_execution_json <flow_name> <status> <exit_code> <started_at> <ended_at> <duration_ms> <steps_file>" >&2
          return 1
      fi

      if [[ ! -f "$steps_file" ]]; then
          echo "report_execution_json: archivo de pasos no encontrado: $steps_file" >&2
          return 1
      fi

      if command -v jq >/dev/null 2>&1; then
          _report_execution_json_jq \
              "$flow_name" "$status" "$exit_code" \
              "$started_at" "$ended_at" "$duration_ms" "$steps_file"
      else
          _report_execution_json_printf \
              "$flow_name" "$status" "$exit_code" \
              "$started_at" "$ended_at" "$duration_ms" "$steps_file"
      fi
  }

  # --- Implementacion con jq ---

  _report_execution_json_jq() {
      local flow_name="$1" status="$2" exit_code="$3"
      local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"

      local steps_ok=0 steps_failed=0
      local steps_json="[]"

      while IFS= read -r line; do
          [[ -z "$line" ]] && continue

          local s_name s_action s_status s_elapsed s_output s_error
          s_name=$(printf '%s' "$line" | cut -f1)
          s_action=$(printf '%s' "$line" | cut -f2)
          s_status=$(printf '%s' "$line" | cut -f3)
          s_elapsed=$(printf '%s' "$line" | cut -f4)
          s_output=$(printf '%s' "$line" | cut -f5)
          s_error=$(printf '%s' "$line" | cut -f6)

          [[ -z "$s_name" ]] && continue

          local step_obj
          step_obj=$(jq -n \
              --arg   name     "$s_name" \
              --arg   action   "$s_action" \
              --arg   st       "$s_status" \
              --argjson ms     "${s_elapsed:-0}" \
              --arg   output   "$s_output" \
              --arg   error    "$s_error" \
              '{name: $name, action: $action, status: $st, elapsed_ms: $ms}
               + (if $output != "" then {output: $output} else {} end)
               + (if $error  != "" then {error:  $error}  else {} end)')

          steps_json=$(printf '%s' "$steps_json" | jq --argjson step "$step_obj" '. + [$step]')

          if [[ "$s_status" == "ok" ]]; then
              ((steps_ok++))
          else
              ((steps_failed++))
          fi
      done < "$steps_file"

      local steps_total=$(( steps_ok + steps_failed ))

      jq -n \
          --arg     name         "$flow_name" \
          --arg     st           "$status" \
          --argjson exit_code    "$exit_code" \
          --arg     started_at   "$started_at" \
          --arg     ended_at     "$ended_at" \
          --argjson duration_ms  "$duration_ms" \
          --argjson total        "$steps_total" \
          --argjson ok           "$steps_ok" \
          --argjson failed       "$steps_failed" \
          --argjson steps        "$steps_json" \
          '{
              name:         $name,
              status:       $st,
              exit_code:    $exit_code,
              started_at:   $started_at,
              ended_at:     $ended_at,
              duration_ms:  $duration_ms,
              steps_total:  $total,
              steps_ok:     $ok,
              steps_failed: $failed,
              steps:        $steps
          }'
  }

  # --- Implementacion con printf (fallback sin jq) ---

  _json_escape_rj() {
      local s="$1"
      s="${s//\\/\\\\}"
      s="${s//\"/\\\"}"
      s="${s//$'\n'/\\n}"
      s="${s//$'\r'/\\r}"
      s="${s//$'\t'/\\t}"
      printf '%s' "$s"
  }

  _report_execution_json_printf() {
      local flow_name="$1" status="$2" exit_code="$3"
      local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"

      local steps_ok=0 steps_failed=0
      local steps_parts=()

      while IFS= read -r line; do
          [[ -z "$line" ]] && continue

          local s_name s_action s_status s_elapsed s_output s_error
          s_name=$(printf '%s' "$line" | cut -f1)
          s_action=$(printf '%s' "$line" | cut -f2)
          s_status=$(printf '%s' "$line" | cut -f3)
          s_elapsed=$(printf '%s' "$line" | cut -f4)
          s_output=$(printf '%s' "$line" | cut -f5)
          s_error=$(printf '%s' "$line" | cut -f6)

          [[ -z "$s_name" ]] && continue

          local part
          part=$(printf '{"name":"%s","action":"%s","status":"%s","elapsed_ms":%s' \
              "$(_json_escape_rj "$s_name")" \
              "$(_json_escape_rj "$s_action")" \
              "$(_json_escape_rj "$s_status")" \
              "${s_elapsed:-0}")

          if [[ -n "$s_output" ]]; then
              part+=",\"output\":\"$(_json_escape_rj "$s_output")\""
          fi
          if [[ -n "$s_error" ]]; then
              part+=",\"error\":\"$(_json_escape_rj "$s_error")\""
          fi
          part+="}"

          steps_parts+=("$part")

          if [[ "$s_status" == "ok" ]]; then
              ((steps_ok++))
          else
              ((steps_failed++))
          fi
      done < "$steps_file"

      local steps_total=$(( steps_ok + steps_failed ))

      local steps_array="["
      local first=1
      for part in "${steps_parts[@]}"; do
          if [[ $first -eq 1 ]]; then
              steps_array+="$part"
              first=0
          else
              steps_array+=",$part"
          fi
      done
      steps_array+="]"

      printf '{"name":"%s","status":"%s","exit_code":%s,"started_at":"%s","ended_at":"%s","duration_ms":%s,"steps_total":%s,"steps_ok":%s,"steps_failed":%s,"steps":%s}\n' \
          "$(_json_escape_rj "$flow_name")" \
          "$(_json_escape_rj "$status")" \
          "$exit_code" \
          "$(_json_escape_rj "$started_at")" \
          "$(_json_escape_rj "$ended_at")" \
          "$duration_ms" \
          "$steps_total" \
          "$steps_ok" \
          "$steps_failed" \
          "$steps_array"
  }

  # Permitir ejecucion directa (no solo sourced)
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      report_execution_json "$@"
  fi

params_schema:
  {"params":[{"name":"flow_name","desc":"nombre del flujo ejecutado"},{"name":"status","desc":"estado final: success, failure, o partial"},{"name":"exit_code","desc":"código de salida: 0, 1, o 2"},{"name":"started_at","desc":"timestamp ISO8601 de inicio"},{"name":"ended_at","desc":"timestamp ISO8601 de fin"},{"name":"duration_ms","desc":"duración total en milisegundos"},{"name":"steps_file","desc":"archivo TSV con resultados de cada paso"}],"output":"JSON estructurado con metadatos de ejecución y array de pasos"}


id: run_steps_bash_shell
name: run_steps
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: run_steps(yaml_file: string, [--strict]) -> string
tags: ["execution","yaml","runner","standard","pipeline"]
uses_functions: ["exit_with_status_bash_shell","report_execution_json_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/run_steps.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c633d74c155f5e8accd1bf4d9b22505e47ce005ad9445740721654fd87aa66aa
description:
  Ejecuta pasos de un YAML generico donde cada step tiene action=command. Lee el YAML con yq, ejecuta cada paso secuencialmente con timeout configurable, captura exit code y output, respeta continue_on_error, y al final reporta JSON estandar a stdout via report_execution_json. Sale con exit code 0 (success), 1 (failure) o 2 (partial). Con --strict mapea partial a failure.

example:
  ```yaml
  # /tmp/test_flow.yaml
  name: test_flow
  steps:
    - name: check
      action: command
      command: "echo hello"
    - name: list
      action: command
      command: "ls /tmp"
      continue_on_error: true
    - name: slow_step
      action: command
      command: "sleep 2"
      timeout_ms: 1000
      continue_on_error: true
  ```

  ```bash
  source bash/functions/shell/run_steps.sh

  run_steps /tmp/test_flow.yaml
  # Imprime JSON a stdout:
  # {
  #   "name": "test_flow",
  #   "status": "partial",
  #   "exit_code": 2,
  #   "started_at": "2026-04-01T10:00:00Z",
  #   "ended_at": "2026-04-01T10:00:01Z",
  #   "duration_ms": 1250,
  #   "steps_total": 3,
  #   "steps_ok": 2,
  #   "steps_failed": 1,
  #   "steps": [
  #     {"name": "check", "action": "command", "status": "ok", "elapsed_ms": 10, "output": "hello"},
  #     {"name": "list", "action": "command", "status": "ok", "elapsed_ms": 15},
  #     {"name": "slow_step", "action": "command", "status": "error", "elapsed_ms": 1001, "error": "timeout: comando excedio 1000ms"}
  #   ]
  # }
  # Sale con exit code 2 (partial)

  run_steps /tmp/test_flow.yaml --strict
  # Igual pero status="failure" y exit code=1
  ```

notes:
  Requiere `yq` (v4+, modo expression `-e`) y `jq` (o la implementacion printf de report_execution_json). Solo soporta `action: command` — otros actions se marcan como error del paso. El campo `timeout_ms` por defecto es 30000 (30s); si el comando excede el timeout, `timeout(1)` sale con exit code 124 y se registra el error correspondiente. El output del comando (stdout+stderr combinados) se sanitiza reemplazando tabuladores y saltos de linea por espacios antes de escribir al TSV temporal. Las funciones `exit_with_status` y `report_execution_json` se cargan via `source` desde el mismo directorio que `run_steps.sh`.

code:
  #!/usr/bin/env bash
  # run_steps — ejecuta pasos de un YAML generico (action=command)

  # run_steps <yaml_file> [--strict]
  #
  # Lee un YAML con la estructura:
  #   name: <run_name>
  #   steps:
  #     - name: <step_name>
  #       action: command
  #       command: "<shell_command>"
  #       continue_on_error: true|false   # opcional, default false
  #       timeout_ms: 30000               # opcional, default 30000
  #
  # Para cada paso de action=command:
  #   - Ejecuta el command con timeout (timeout_ms ms)
  #   - Captura exit code, stdout+stderr y elapsed time
  #   - Si falla y continue_on_error=false → aborta
  #   - Acumula resultados en TSV temporal
  #
  # Al final:
  #   - Llama a report_execution_json para generar JSON a stdout
  #   - Llama a exit_with_status para determinar el exit code
  #
  # --strict: mapea partial (2) a failure (1) en status y exit code
  #
  # Requiere: yq, jq (jq opcional si report_execution_json tiene fallback printf)
  run_steps() {
      local yaml_file="$1"
      local strict=0

      if [[ "${2:-}" == "--strict" ]]; then
          strict=1
      fi

      # --- validaciones previas ---
      if [[ -z "$yaml_file" ]]; then
          echo "run_steps: yaml_file requerido" >&2
          return 1
      fi

      if [[ ! -f "$yaml_file" ]]; then
          echo "run_steps: archivo '$yaml_file' no existe" >&2
          return 1
      fi

      if ! command -v yq &>/dev/null; then
          echo "run_steps: yq no encontrado en PATH" >&2
          return 1
      fi

      # --- cargar funciones del estandar ---
      local script_dir
      script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      # shellcheck source=bash/functions/shell/exit_with_status.sh
      source "$script_dir/exit_with_status.sh"
      # shellcheck source=bash/functions/shell/report_execution_json.sh
      source "$script_dir/report_execution_json.sh"

      # --- leer nombre del run ---
      local run_name
      run_name=$(yq e '.name // "unnamed"' "$yaml_file")

      # --- contar pasos ---
      local step_count
      step_count=$(yq e '.steps | length' "$yaml_file")

      if [[ -z "$step_count" || "$step_count" -eq 0 ]]; then
          echo "run_steps: el YAML no tiene steps" >&2
          return 1
      fi

      # --- archivo TSV temporal para acumular resultados ---
      # columnas: name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
      local tsv_file
      tsv_file=$(mktemp /tmp/run_steps_XXXXXX.tsv)
      # shellcheck disable=SC2064
      trap "rm -f '$tsv_file'" EXIT

      local ok_steps=0
      local failed_steps=0
      local started_at
      started_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
      local t_run_start
      t_run_start=$(date +%s%3N)

      # --- iterar sobre cada paso ---
      local i
      for (( i=0; i<step_count; i++ )); do
          local step_name step_action step_command step_continue step_timeout_ms

          step_name=$(yq e ".steps[$i].name // \"step_$i\"" "$yaml_file")
          step_action=$(yq e ".steps[$i].action // \"command\"" "$yaml_file")
          step_command=$(yq e ".steps[$i].command // \"\"" "$yaml_file")
          step_continue=$(yq e ".steps[$i].continue_on_error // false" "$yaml_file")
          step_timeout_ms=$(yq e ".steps[$i].timeout_ms // 30000" "$yaml_file")

          # convertir timeout de ms a segundos enteros (timeout(1) usa segundos)
          local timeout_sec
          timeout_sec=$(( (step_timeout_ms + 999) / 1000 ))

          # solo soportamos action=command
          if [[ "$step_action" != "command" ]]; then
              local err_msg="action '$step_action' no soportada (solo command)"
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "$step_action" "error" "0" "" "$err_msg" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: paso '$step_name' — $err_msg — abortando" >&2
                  break
              fi
              continue
          fi

          if [[ -z "$step_command" ]]; then
              local err_msg="campo 'command' vacio en paso '$step_name'"
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "error" "0" "" "$err_msg" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: $err_msg — abortando" >&2
                  break
              fi
              continue
          fi

          # ejecutar con timeout y capturar output + tiempos
          local t_start t_end elapsed_ms step_output step_exit

          t_start=$(date +%s%3N)
          step_output=$(timeout "$timeout_sec" bash -c "$step_command" 2>&1)
          step_exit=$?
          t_end=$(date +%s%3N)
          elapsed_ms=$(( t_end - t_start ))

          # timeout(1) sale con 124 cuando agota el tiempo
          local step_error=""
          if [[ "$step_exit" -eq 124 ]]; then
              step_error="timeout: comando excedio ${step_timeout_ms}ms"
          fi

          # escapar tabuladores y saltos de linea en el output para el TSV
          local safe_output
          safe_output=$(printf '%s' "$step_output" | tr '\t' ' ' | tr '\n' ' ')

          if [[ "$step_exit" -eq 0 ]]; then
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "ok" "$elapsed_ms" "$safe_output" "" >> "$tsv_file"
              ok_steps=$((ok_steps + 1))
          else
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "error" "$elapsed_ms" "$safe_output" "$step_error" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: paso '$step_name' fallo (exit $step_exit) — abortando" >&2
                  break
              fi
          fi
      done

      local t_run_end
      t_run_end=$(date +%s%3N)
      local total_duration_ms=$(( t_run_end - t_run_start ))
      local ended_at
      ended_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
      local total_steps=$(( ok_steps + failed_steps ))

      # --- calcular status y exit code ---
      local status_code
      status_code=$(exit_with_status "$total_steps" "$ok_steps" "$failed_steps")

      local run_status
      case "$status_code" in
          0) run_status="success" ;;
          1) run_status="failure" ;;
          2)
              if [[ "$strict" -eq 1 ]]; then
                  run_status="failure"
                  status_code=1
              else
                  run_status="partial"
              fi
              ;;
          *) run_status="failure"; status_code=1 ;;
      esac

      # --- generar JSON a stdout ---
      report_execution_json \
          "$run_name" \
          "$run_status" \
          "$status_code" \
          "$started_at" \
          "$ended_at" \
          "$total_duration_ms" \
          "$tsv_file"

      return "$status_code"
  }

params_schema:
  {"params":[{"name":"yaml_file","desc":"archivo YAML con especificación de pasos"},{"name":"--strict","desc":"flag opcional para mapear partial a failure"}],"output":"JSON de ejecución con detalles de cada paso"}


id: validate_registry_paths_bash_shell
name: validate_registry_paths
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: validate_registry_paths(db_path: string, table: string, root_dir: string) -> tsv_stdout
tags: ["registry","validation","paths","audit","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/validate_registry_paths.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: eb3eacaa9aaf99f8e96758d349a0d22db148b7ff821d05a5a0ad4d44cfac23b4
description:
  Consulta registry.db y verifica que cada file_path apunte a un archivo existente en disco. Imprime a stdout las rutas rotas en formato TSV (id, file_path, domain, tabla).

example:
  ```bash
  source validate_registry_paths.sh
  validate_registry_paths /home/lucas/fn_registry/registry.db functions /home/lucas/fn_registry

  # Output (TSV):
  # cdp_click_go_browser	functions/infra/cdp_click.go	browser	functions
  ```

notes:
  Impura porque lee el filesystem y la base de datos. No modifica nada — solo reporta.

  La salida TSV es consumible por otros scripts o pipelines. Si no hay rutas rotas, no imprime nada.

code:
  #!/usr/bin/env bash
  # validate_registry_paths
  # -----------------------
  # Consulta registry.db y verifica que cada file_path apunte a un archivo existente.
  # Recibe la ruta a registry.db y la tabla a validar (functions o types).
  # Imprime a stdout las lineas con rutas rotas en formato TSV:
  #   id<TAB>file_path<TAB>domain<TAB>tabla
  # Exit code 0 siempre (es una consulta, no una asercion).
  #
  # USO (sourced):
  #   source validate_registry_paths.sh
  #   validate_registry_paths /ruta/registry.db functions /ruta/raiz
  #
  # USO (directo):
  #   bash validate_registry_paths.sh /ruta/registry.db functions /ruta/raiz

  validate_registry_paths() {
      local db_path="$1"
      local table="$2"
      local root_dir="$3"

      if [[ -z "$db_path" || -z "$table" || -z "$root_dir" ]]; then
          echo "validate_registry_paths: uso: validate_registry_paths <db_path> <table> <root_dir>" >&2
          return 1
      fi

      if [[ "$table" != "functions" && "$table" != "types" ]]; then
          echo "validate_registry_paths: tabla debe ser 'functions' o 'types'" >&2
          return 1
      fi

      local broken=0
      while IFS='|' read -r id fp domain; do
          if [[ ! -f "$root_dir/$fp" ]]; then
              printf '%s\t%s\t%s\t%s\n' "$id" "$fp" "$domain" "$table"
              ((broken++))
          fi
      done < <(sqlite3 "$db_path" "SELECT id, file_path, domain FROM $table ORDER BY id;")

      return 0
  }

  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      validate_registry_paths "$@"
  fi

params_schema:
  {"params":[{"name":"db_path","desc":"ruta a registry.db"},{"name":"table","desc":"tabla a validar: functions o types"},{"name":"root_dir","desc":"directorio raíz del repositorio"}],"output":"TSV con id, file_path, domain, tabla de rutas inválidas"}


id: dashboard_grid_cpp_core
name: dashboard_grid
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
tags: ["imgui","grid","layout","dashboard","responsive"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/dashboard_grid.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: af6d4a2555dc0b15f4cadb53fe5f83a09ae139b9ceb86c6de3eb5d36ce3fef71
signature:
  void dashboard_grid_begin(int columns = 2, float spacing = 8.0f); void dashboard_grid_next(); void dashboard_grid_end()

description:
  Grid de N columnas para distribuir widgets de dashboard automaticamente con spacing uniforme entre columnas

notes:
  - Llamar `dashboard_grid_next()` entre cada par de widgets, **no** antes del primero ni despues del ultimo.
  - El numero de `dashboard_grid_next()` puede ser mayor que `columns - 1`: el grid hace wrap automatico a la siguiente fila.
  - Combina bien con `dashboard_panel_begin`/`dashboard_panel_end` para crear dashboards con paneles alineados en cuadricula.
  - Si `columns <= 0` se fuerza a 1 para evitar division por cero.

documentation:
  # dashboard_grid

  Divide el ancho disponible en N columnas con spacing uniforme y posiciona cada widget en su celda automaticamente. Soporta grids anidados mediante un stack interno de estado.

  ## Uso

  ```cpp
  dashboard_grid_begin(3, 8.0f);

  // Celda 0
  dashboard_panel_begin("CPU");
  ImGui::Text("%.1f %%", cpu_pct);
  dashboard_panel_end();
  dashboard_grid_next();

  // Celda 1
  dashboard_panel_begin("Memory");
  ImGui::Text("%.0f MB", mem_mb);
  dashboard_panel_end();
  dashboard_grid_next();

  // Celda 2
  dashboard_panel_begin("Disk");
  ImGui::Text("%.0f GB", disk_gb);
  dashboard_panel_end();

  dashboard_grid_end();
  ```

  ## Implementacion

  ### Calculo de ancho

  ```
  col_width = (available_width - spacing * (columns - 1)) / columns
  ```

  `available_width` se obtiene de `ImGui::GetContentRegionAvail().x` en el momento de `dashboard_grid_begin`.

  ### Mecanica de celdas

  Cada celda es un `BeginGroup`/`EndGroup` con `PushItemWidth(col_width)` para que los widgets internos respeten el ancho de columna. Al llamar `dashboard_grid_next`:

  1. Se cierra la celda actual (`PopItemWidth`, `EndGroup`).
  2. Se incrementa el contador interno.
  3. Si `counter % columns != 0` se emite `SameLine(0, spacing)` para continuar en la misma fila.
  4. Si `counter % columns == 0` no se emite `SameLine`: ImGui pasa a la siguiente fila automaticamente.
  5. Se abre la nueva celda (`BeginGroup`, `PushItemWidth`).

  ### Grids anidados

  El estado (columnas, spacing, col_width, counter) se guarda en `g_grid_stack` (un `std::vector` de structs en un namespace anonimo). Cada llamada a `dashboard_grid_begin` hace `push_back` y `dashboard_grid_end` hace `pop_back`, permitiendo anidar grids sin conflicto.

code:
  #include "dashboard_grid.h"
  #include <imgui.h>
  #include <vector>

  // Internal state stack to support nested grids.
  namespace {

  struct GridState {
      int   columns;
      float spacing;
      float col_width;
      int   counter;  // number of dashboard_grid_next() calls so far
  };

  static std::vector<GridState> g_grid_stack;

  } // namespace

  void dashboard_grid_begin(int columns, float spacing) {
      if (columns < 1) columns = 1;

      float available = ImGui::GetContentRegionAvail().x;
      float col_width = (available - spacing * static_cast<float>(columns - 1))
                        / static_cast<float>(columns);
      if (col_width < 1.0f) col_width = 1.0f;

      g_grid_stack.push_back({columns, spacing, col_width, 0});

      ImGui::BeginGroup();
      ImGui::PushItemWidth(col_width);
  }

  void dashboard_grid_next() {
      if (g_grid_stack.empty()) return;

      GridState& s = g_grid_stack.back();

      ImGui::PopItemWidth();
      ImGui::EndGroup();

      s.counter++;

      if (s.counter % s.columns != 0) {
          // Same row: advance horizontally.
          ImGui::SameLine(0.0f, s.spacing);
      }
      // If counter % columns == 0 the next BeginGroup starts a new row automatically.

      ImGui::BeginGroup();
      ImGui::PushItemWidth(s.col_width);
  }

  void dashboard_grid_end() {
      if (g_grid_stack.empty()) return;

      ImGui::PopItemWidth();
      ImGui::EndGroup();

      g_grid_stack.pop_back();
  }

params_schema:
  {"params":[{"name":"columns","desc":"Numero de columnas del grid (minimo 1); el ancho disponible se divide uniformemente entre ellas"},{"name":"spacing","desc":"Espacio horizontal entre columnas en pixels"}],"output":"Layout de grid aplicado al contenido entre dashboard_grid_begin/end; cada celda recibe un ancho uniforme calculado a partir del espacio disponible"}


id: dashboard_panel_cpp_core
name: dashboard_panel
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
tags: ["imgui","panel","container","layout","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/dashboard_panel.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 2471531214723d7570258d0eb1a0c7a19e9e72c7ddb70b670ecbe3de4094559f
signature:
  bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f); void dashboard_panel_end()

description:
  Contenedor estilizado tipo panel para dashboards con titulo, bordes redondeados y tamaño minimo configurable

notes:
  - El titulo actua como ID de ImGui: dos paneles con el mismo titulo en el mismo frame se comportan como uno solo. Usar `##` para diferenciar IDs si se repite el texto: `"Revenue##panel1"`.
  - `AutoResizeY` hace que el panel crezca verticalmente con su contenido; `min_height` establece el piso.
  - El patron begin/end es idomatico en ImGui: `end` debe llamarse siempre para hacer pop de los estilos, aunque `begin` retorne false.

documentation:
  # dashboard_panel

  Panel estilizado para dashboards ImGui. Envuelve un `BeginChild`/`EndChild` con estilos predefinidos: fondo oscuro (`#1F1F26`), bordes redondeados (5 px), borde visible y separador bajo el titulo.

  ## Uso

  ```cpp
  if (dashboard_panel_begin("Revenue", 300.0f, 200.0f)) {
      line_plot("Revenue", xs, ys, N);
  }
  dashboard_panel_end(); // siempre llamar
  ```

  ## Implementacion

  - `PushStyleVar` aplica `ChildRounding = 5.0f` y `ChildBorderSize = 1.0f`
  - `PushStyleColor` establece el fondo del child a `(0.12, 0.12, 0.15, 1.0)`
  - `BeginChild` con `ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY`
  - Titulo con `TextUnformatted` seguido de `Separator`
  - `dashboard_panel_end` hace `EndChild`, `PopStyleColor(1)`, `PopStyleVar(2)`

code:
  #include "dashboard_panel.h"
  #include <imgui.h>

  bool dashboard_panel_begin(const char* title, float min_width, float min_height) {
      ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
      ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
      ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.15f, 1.0f));

      ImVec2 size(min_width > 0.0f ? min_width : 0.0f,
                  min_height > 0.0f ? min_height : 0.0f);

      ImGui::BeginChild(title, size, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY);

      ImGui::TextUnformatted(title);
      ImGui::Separator();

      return true;
  }

  void dashboard_panel_end() {
      ImGui::EndChild();
      ImGui::PopStyleColor(1);
      ImGui::PopStyleVar(2);
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del panel, tambien sirve como ID de ImGui para distinguir multiples paneles"},{"name":"min_width","desc":"Ancho minimo del panel en pixels (0 = sin restriccion)"},{"name":"min_height","desc":"Alto minimo del panel en pixels (0 = sin restriccion)"}],"output":"true si el panel es visible y se debe renderizar contenido; llamar siempre dashboard_panel_end() independientemente del valor de retorno"}


id: docking_layout_cpp_core
name: docking_layout
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: ImGuiID docking_layout(DockPreset preset = DockPreset::Default)
description: Configura un docking space con presets de layout predefinidos para dashboards
tags: ["imgui","docking","layout","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/docking_layout.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 2beea4a5dadc1a44cb60268bf7792617e35516eafb64f6cbf807be84ab53a349
example:
  ```cpp
  void render_fn() {
      ImGuiID dock = docking_layout(DockPreset::SidebarLeft);

      ImGui::Begin("Filters");
      // controles del sidebar
      ImGui::End();

      ImGui::Begin("Main");
      // contenido principal
      ImGui::End();
  }
  ```

notes:
  Requiere que `ImGuiConfigFlags_DockingEnable` este activo en `ImGui::GetIO().ConfigFlags` (habilitado por `app_base.cpp`). El preset se aplica solo en el primer frame (static bool). `imgui_internal.h` es necesario para `DockBuilder*`.

documentation:
  # docking_layout

  Configura un docking space fullscreen sobre el viewport principal con presets de layout predefinidos.

  Usa `DockSpaceOverViewport` para crear el dockspace y, en el primer frame, aplica el preset con `DockBuilderSplitNode`. Llamar una vez al inicio de cada frame, antes de renderizar las ventanas hijas.

  ## Presets disponibles

  | Preset | Layout |
  |---|---|
  | Default | Dockspace completo sin divisiones |
  | TwoColumns | Izquierda 60% / Derecha 40% |
  | ThreeColumns | Tres columnas iguales ~33% |
  | SidebarLeft | Sidebar 25% / Main 75% |
  | SidebarRight | Main 75% / Sidebar 25% |
  | TopBottom | Arriba 60% / Abajo 40% |
  | Dashboard | Grid 2x2 de cuatro paneles |

code:
  #include "core/docking_layout.h"
  #include "imgui_internal.h"

  ImGuiID docking_layout(DockPreset preset) {
      const ImGuiViewport* viewport = ImGui::GetMainViewport();
      ImGuiID dockspace_id = ImGui::DockSpaceOverViewport(0, viewport);

      static bool initialized = false;
      if (initialized) {
          return dockspace_id;
      }
      initialized = true;

      if (preset == DockPreset::Default) {
          return dockspace_id;
      }

      ImGui::DockBuilderRemoveNode(dockspace_id);
      ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
      ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);

      if (preset == DockPreset::TwoColumns) {
          ImGuiID right;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.6f, nullptr, &right);

      } else if (preset == DockPreset::ThreeColumns) {
          ImGuiID center, right;
          ImGuiID left_node;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.33f, &left_node, &center);
          ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.5f, nullptr, &right);

      } else if (preset == DockPreset::SidebarLeft) {
          ImGuiID main;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.25f, nullptr, &main);

      } else if (preset == DockPreset::SidebarRight) {
          ImGuiID sidebar;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.25f, &sidebar, nullptr);

      } else if (preset == DockPreset::TopBottom) {
          ImGuiID bottom;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.6f, nullptr, &bottom);

      } else if (preset == DockPreset::Dashboard) {
          ImGuiID top, bottom;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.5f, &top, &bottom);
          ImGui::DockBuilderSplitNode(top,    ImGuiDir_Left, 0.5f, nullptr, nullptr);
          ImGui::DockBuilderSplitNode(bottom, ImGuiDir_Left, 0.5f, nullptr, nullptr);
      }

      ImGui::DockBuilderFinish(dockspace_id);
      return dockspace_id;
  }

params_schema:
  {"params":[{"name":"preset","desc":"Layout predefinido a aplicar (Default, TwoColumns, ThreeColumns, SidebarLeft, SidebarRight, TopBottom, Dashboard)"}],"output":"ID del dockspace creado, usable con ImGui::SetNextWindowDockID"}


id: fps_overlay_cpp_core
name: fps_overlay
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: void fps_overlay()
tags: ["imgui","fps","overlay","profiling","debug"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/fps_overlay.cpp
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 1a1d25ddac5db7eef21dee59a15e38cd6ace1059e873d19556df7319a5b801d8
params_schema: {"output":"Renderiza el overlay de FPS en el frame ImGui actual"}
description:
  Renderiza un overlay de FPS y frametime en la esquina superior derecha, con soporte opcional de Tracy

documentation:
  # fps_overlay

  Muestra FPS y frametime (ms) en una ventana semi-transparente en la esquina superior derecha.

  Si se compila con `TRACY_ENABLE`, incluye un `ZoneScoped` para profiling con Tracy.

code:
  #include "core/fps_overlay.h"
  #include "imgui.h"

  #ifdef TRACY_ENABLE
  #include "tracy/Tracy.hpp"
  #endif

  void fps_overlay() {
  #ifdef TRACY_ENABLE
      ZoneScoped;
  #endif

      ImGuiIO& io = ImGui::GetIO();
      ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
                             | ImGuiWindowFlags_AlwaysAutoResize
                             | ImGuiWindowFlags_NoSavedSettings
                             | ImGuiWindowFlags_NoFocusOnAppearing
                             | ImGuiWindowFlags_NoNav
                             | ImGuiWindowFlags_NoMove;

      const float pad = 10.0f;
      const ImGuiViewport* viewport = ImGui::GetMainViewport();
      ImVec2 pos(viewport->WorkPos.x + viewport->WorkSize.x - pad,
                 viewport->WorkPos.y + pad);
      ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 0.0f));
      ImGui::SetNextWindowBgAlpha(0.65f);

      if (ImGui::Begin("##fps_overlay", nullptr, flags)) {
          ImGui::Text("%.1f FPS", io.Framerate);
          ImGui::Text("%.3f ms", 1000.0f / io.Framerate);
      }
      ImGui::End();
  }


id: fullscreen_window_cpp_core
name: fullscreen_window
kind: component
lang: cpp
domain: core
version: 0.1.0
purity: pure
signature: bool fullscreen_window_begin(const char* id = "##fullscreen"); void fullscreen_window_end()
tags: ["imgui","layout","fullscreen","window"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/fullscreen_window.cpp
created_at: 2026-04-07T22:24:27Z
updated_at: 2026-04-07T22:24:27Z
props: []
emits: []
framework: imgui
variant: []
content_hash: cf3510f5959117e66eb340deae1d7b929588aa536f25ab649c655615a3c5a289
description:
  Ventana ImGui fullscreen sin decoraciones que ocupa todo el viewport, elimina la necesidad de usar el sistema de ventanas interno

notes:
  - Pura en el sentido de que no hace I/O ni tiene estado propio; solo configura el estado next-frame de ImGui.
  - `WorkPos`/`WorkSize` respetan los menu bars del sistema operativo (en plataformas que los tienen). Para ocupar literalmente toda la pantalla usar `Pos`/`Size` del viewport.
  - Compatible con `dashboard_grid` y `dashboard_panel`: el fullscreen_window actua como contenedor raiz y los paneles/grids se renderizan dentro.
  - El patron begin/end es idiomatico en ImGui: `end` debe llamarse siempre para cerrar la ventana correctamente, aunque `begin` retorne false.

documentation:
  # fullscreen_window

  Wrapper que crea una ventana ImGui que ocupa exactamente el viewport de trabajo (`WorkPos` / `WorkSize`). Elimina todas las decoraciones: title bar, resize grip, move, collapse. Ideal como capa raiz de una aplicacion ImGui donde el contenido propio gestiona el layout.

  ## Uso

  ```cpp
  if (fullscreen_window_begin()) {
      // todo el layout de la app va aqui
      dashboard_grid_begin(3, 8.0f);
      // ...
      dashboard_grid_end();
  }
  fullscreen_window_end(); // siempre llamar
  ```

  Con ID explicito (si se necesitan multiples capas):

  ```cpp
  if (fullscreen_window_begin("##background")) {
      render_background();
  }
  fullscreen_window_end();
  ```

  ## Implementacion

  - `GetMainViewport()` obtiene el viewport principal (compatible con viewports multi-monitor de ImGui)
  - `SetNextWindowPos(vp->WorkPos)` posiciona en el area de trabajo (excluye menu bars del OS)
  - `SetNextWindowSize(vp->WorkSize)` ocupa exactamente el area disponible
  - Flags: `NoTitleBar | NoResize | NoMove | NoCollapse | NoBringToFrontOnFocus | NoNavFocus`
  - `NoBringToFrontOnFocus` y `NoNavFocus` evitan que la ventana fullscreen robe el foco de ventanas superpuestas

code:
  #include "core/fullscreen_window.h"
  #include "imgui.h"

  bool fullscreen_window_begin(const char* id) {
      const ImGuiViewport* vp = ImGui::GetMainViewport();
      ImGui::SetNextWindowPos(vp->WorkPos);
      ImGui::SetNextWindowSize(vp->WorkSize);
      return ImGui::Begin(id, nullptr,
          ImGuiWindowFlags_NoTitleBar      |
          ImGuiWindowFlags_NoResize        |
          ImGuiWindowFlags_NoMove          |
          ImGuiWindowFlags_NoCollapse      |
          ImGuiWindowFlags_NoBringToFrontOnFocus |
          ImGuiWindowFlags_NoNavFocus);
  }

  void fullscreen_window_end() {
      ImGui::End();
  }

params_schema:
  {"params":[{"name":"id","desc":"Identificador ImGui de la ventana, default ##fullscreen (el prefijo ## oculta el texto del titulo)"}],"output":"true si la ventana es visible (siempre true en fullscreen); llamar siempre fullscreen_window_end() independientemente del valor de retorno"}


id: graph_spatial_hash_cpp_core
name: graph_spatial_hash
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: SpatialHash(float cell_size, int table_size)
tags: ["spatial","hash","acceleration","graph","query","hittest"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: cpp/functions/core/graph_spatial_hash.cpp
created_at: 2026-04-07T22:04:57Z
updated_at: 2026-04-07T22:04:57Z
props: []
emits: []
framework: imgui
variant: []
content_hash: a5192801758a6cfb6edc9d9ed505000f8013c39539a6bf4bdff5a879177e81b4
description:
  Spatial hash grid para busqueda O(1) de puntos por posicion, usado para hit-testing de nodos en grafos

notes:
  - No es thread-safe: disenado para uso single-threaded en el hilo de render de ImGui.
  - `build()` debe llamarse cada frame si las posiciones de los nodos cambian.
  - `cell_size` debe ser >= al radio maximo de los nodos para que la vecindad 3x3 capture todos los candidatos posibles.
  - La distancia efectiva al nodo se calcula como `dist(centro) - size`, de modo que el hit-test funciona correctamente para nodos de distintos tamaños.
  - No implementa `operator=` ni copy constructor (deleted) para evitar doble-free de los arrays internos.

documentation:
  # graph_spatial_hash

  Spatial hash grid para busqueda eficiente de nodos en grafos por posicion 2D. Util para hit-testing de nodos en editores de grafos basados en ImGui, donde se necesita saber que nodo esta bajo el cursor en cada frame.

  ## Uso

  ```cpp
  SpatialHash sh(20.0f, 4096);

  // Reconstruir cada frame (o cuando cambian posiciones)
  sh.build(node_xs, node_ys, node_sizes, node_count);

  // Hit-test: nodo mas cercano al cursor
  float dist;
  int hovered = sh.query_nearest(mouse_x, mouse_y, 30.0f, &dist);

  // Seleccion por area: todos los nodos dentro de un radio
  int results[256];
  int count = sh.query_radius(center_x, center_y, radius, results, 256);
  ```

  ## Estructura interna

  - `buckets[table_size]`: cada entrada contiene el indice del primer slot de la cadena, o -1 si el bucket esta vacio.
  - `entries[2 * entry_capacity]`: pares `[node_index, next_in_chain]` almacenados como flat array. Se expande con `realloc` si el numero de nodos supera la capacidad inicial (256).
  - El snapshot de posiciones (`xs`, `ys`, `sizes`) se copia internamente en `build()` para que `query_nearest` y `query_radius` puedan calcular distancias sin recibir los arrays como parametro.

  ## Hash function

  ```
  hash(cx, cy) = (cx * 73856093 ^ cy * 19349663) % table_size
  ```

  Donde `cx = floor(x / cell_size)`, `cy = floor(y / cell_size)`.

code:
  #include "graph_spatial_hash.h"

  #include <cmath>
  #include <cstdlib>
  #include <cstring>

  // ---------------------------------------------------------------------------
  // Helpers
  // ---------------------------------------------------------------------------

  static inline int floor_div(float v, float cell) {
      return static_cast<int>(std::floor(v / cell));
  }

  static inline float sq(float x) { return x * x; }

  // ---------------------------------------------------------------------------
  // Snapshot de posiciones (almacenado internamente en build)
  // La interfaz del header no pasa xs/ys a los metodos de query, por lo que
  // SpatialHash conserva copias internas para calcular distancias.
  // ---------------------------------------------------------------------------

  struct SpatialHashSnap {
      float* xs    = nullptr;
      float* ys    = nullptr;
      float* sizes = nullptr;
      int    count = 0;
  };

  // Almacenamos el snapshot en memoria contigua al final del bloque de entries,
  // para no añadir campos al header publico. Usamos un puntero opaco en un
  // campo reservado. Como el header ya esta fijado, guardamos el snapshot como
  // una variable estatica por instancia — aceptable para uso single-threaded
  // tipico de ImGui. Para multi-instancia se puede usar un map.
  //
  // En la practica, la convencion del registry es que SpatialHash se crea una
  // vez por frame de ImGui y se usa en el mismo hilo de render.

  static SpatialHashSnap g_snap;   // snapshot mas reciente

  // ---------------------------------------------------------------------------
  // SpatialHash
  // ---------------------------------------------------------------------------

  int SpatialHash::cell_hash(int cx, int cy) const {
      unsigned int h = static_cast<unsigned int>(cx) * 73856093u
                     ^ static_cast<unsigned int>(cy) * 19349663u;
      return static_cast<int>(h % static_cast<unsigned int>(table_size));
  }

  SpatialHash::SpatialHash(float cell_size_, int table_size_)
      : cell_size(cell_size_)
      , table_size(table_size_)
      , entry_count(0)
      , entry_capacity(256)
  {
      buckets = static_cast<int*>(std::malloc(
          static_cast<std::size_t>(table_size) * sizeof(int)));

      entries = static_cast<int*>(std::malloc(
          static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));

      std::memset(buckets, -1,
          static_cast<std::size_t>(table_size) * sizeof(int));
  }

  SpatialHash::~SpatialHash() {
      std::free(buckets);
      std::free(entries);

      std::free(g_snap.xs);
      std::free(g_snap.ys);
      std::free(g_snap.sizes);
      g_snap = {};
  }

  void SpatialHash::build(const float* xs, const float* ys, const float* sizes, int count) {
      // --- Limpiar tabla ---
      std::memset(buckets, -1,
          static_cast<std::size_t>(table_size) * sizeof(int));
      entry_count = 0;

      // --- Snapshot de posiciones para queries ---
      if (g_snap.count < count) {
          std::free(g_snap.xs);
          std::free(g_snap.ys);
          std::free(g_snap.sizes);
          g_snap.xs    = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
          g_snap.ys    = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
          g_snap.sizes = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
      }
      std::memcpy(g_snap.xs,    xs,    static_cast<std::size_t>(count) * sizeof(float));
      std::memcpy(g_snap.ys,    ys,    static_cast<std::size_t>(count) * sizeof(float));
      std::memcpy(g_snap.sizes, sizes, static_cast<std::size_t>(count) * sizeof(float));
      g_snap.count = count;

      // --- Insertar nodos en la tabla hash ---
      for (int i = 0; i < count; ++i) {
          int cx     = floor_div(xs[i], cell_size);
          int cy     = floor_div(ys[i], cell_size);
          int bucket = cell_hash(cx, cy);

          // Crecer entries si necesario
          if (entry_count >= entry_capacity) {
              entry_capacity *= 2;
              entries = static_cast<int*>(std::realloc(
                  entries,
                  static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));
          }

          // Insertar al frente de la cadena encadenada
          int slot = entry_count++;
          entries[slot * 2 + 0] = i;               // node_index
          entries[slot * 2 + 1] = buckets[bucket];  // next_in_chain
          buckets[bucket] = slot;
      }
  }

  int SpatialHash::query_nearest(float qx, float qy, float radius, float* out_dist) const {
      int   best_idx = -1;
      float best_d   = radius;  // umbral: solo aceptamos dentro del radio

      int qcx = floor_div(qx, cell_size);
      int qcy = floor_div(qy, cell_size);

      // Escanear vecindad 3x3 de celdas
      for (int dy = -1; dy <= 1; ++dy) {
          for (int dx = -1; dx <= 1; ++dx) {
              int bucket = cell_hash(qcx + dx, qcy + dy);
              int slot   = buckets[bucket];
              while (slot != -1) {
                  int ni      = entries[slot * 2 + 0];
                  float ex    = g_snap.xs[ni] - qx;
                  float ey    = g_snap.ys[ni] - qy;
                  float dist  = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
                  if (dist < best_d) {
                      best_d   = dist;
                      best_idx = ni;
                  }
                  slot = entries[slot * 2 + 1];
              }
          }
      }

      if (out_dist && best_idx != -1)
          *out_dist = best_d;

      return best_idx;
  }

  int SpatialHash::query_radius(float qx, float qy, float radius, int* out, int max_out) const {
      int found = 0;

      int qcx = floor_div(qx, cell_size);
      int qcy = floor_div(qy, cell_size);

      for (int dy = -1; dy <= 1; ++dy) {
          for (int dx = -1; dx <= 1; ++dx) {
              int bucket = cell_hash(qcx + dx, qcy + dy);
              int slot   = buckets[bucket];
              while (slot != -1 && found < max_out) {
                  int ni     = entries[slot * 2 + 0];
                  float ex   = g_snap.xs[ni] - qx;
                  float ey   = g_snap.ys[ni] - qy;
                  float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
                  if (dist <= radius)
                      out[found++] = ni;
                  slot = entries[slot * 2 + 1];
              }
          }
      }

      return found;
  }

params_schema:
  {"params":[{"name":"cell_size","desc":"Tamaño de cada celda del grid espacial (debe ser \u003e= radio maximo de nodos)"},{"name":"table_size","desc":"Numero de buckets en la tabla hash (potencia de 2 recomendada para distribucion uniforme)"}],"output":"Estructura de hash espacial lista para queries de puntos cercanos"}


id: memory_overlay_cpp_core
name: memory_overlay
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: void memory_overlay()
description: Renderiza un overlay de estadisticas de memoria (RSS, peak, vsize) en la esquina inferior derecha
tags: ["imgui","memory","overlay","debug","dashboard","profiling"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
example:
  ```cpp
  // En el loop de render, dentro de un frame ImGui:
  fps_overlay();
  memory_overlay();
  ```

tests: []
file_path: cpp/functions/core/memory_overlay.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 7c325bf54f798be7495f41e26cb8c6110baa191bcf11fc37e8f2df409e90b7d7
params_schema: {"output":"Renderiza el overlay de memoria en el frame ImGui actual"}
notes:
  - La función sigue la misma convención que `fps_overlay`: mismos flags de ventana, mismo alpha (0.65).
  - Posición: esquina inferior derecha (`pivot = {1,1}`), separada 10 px del borde.
  - La lectura de `/proc/self/status` es I/O local de ~0.1 ms, amortizada a 1 Hz — despreciable.
  - Con `TRACY_ENABLE` activo, la función registra una zona `ZoneScoped` para que aparezca en el profiler.
  - Si la plataforma no puede leer las estadísticas, muestra "Memory: N/A" en gris.

documentation:
  # memory_overlay

  Muestra estadísticas de memoria del proceso en una ventana semi-transparente en la esquina inferior derecha. Complementa `fps_overlay` para un dashboard mínimo de rendimiento.

  Las métricas se leen como máximo una vez por segundo y se cachean en una variable estática local, evitando I/O excesivo en el hot path de render.

  ## Métricas mostradas

  | Campo | Linux | Windows |
  |---|---|---|
  | RSS (MB) | `VmRSS` de `/proc/self/status` | `WorkingSetSize` (PSAPI) |
  | Peak RSS (MB) | `VmPeak` de `/proc/self/status` | `PeakWorkingSetSize` (PSAPI) |
  | Virtual Size (MB) | `VmSize` de `/proc/self/status` | N/A (no mostrado) |

  En Windows se requiere enlazar `psapi.lib` (el `.cpp` incluye `#pragma comment(lib, "psapi.lib")`).

code:
  #include "core/memory_overlay.h"
  #include "imgui.h"

  #ifdef TRACY_ENABLE
  #include "tracy/Tracy.hpp"
  #endif

  #ifdef _WIN32
  #define WIN32_LEAN_AND_MEAN
  #include <windows.h>
  #include <psapi.h>
  #pragma comment(lib, "psapi.lib")
  #else
  #include <cstdio>
  #endif

  #include <cstring>

  namespace {

  struct MemStats {
      long rss_kb    = -1; // Resident Set Size
      long peak_kb   = -1; // Peak RSS
      long vsize_kb  = -1; // Virtual memory size
  };

  static MemStats s_cached;
  static double   s_last_sample = -1.0;

  #ifdef _WIN32
  static MemStats sample_memory() {
      MemStats s;
      PROCESS_MEMORY_COUNTERS pmc{};
      if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
          s.rss_kb  = static_cast<long>(pmc.WorkingSetSize       / 1024);
          s.peak_kb = static_cast<long>(pmc.PeakWorkingSetSize   / 1024);
          s.vsize_kb = -1; // not easily available on Windows without VirtualQuery loop
      }
      return s;
  }
  #else
  static MemStats sample_memory() {
      MemStats s;
      FILE* f = std::fopen("/proc/self/status", "r");
      if (!f) return s;

      char line[128];
      while (std::fgets(line, sizeof(line), f)) {
          long val = 0;
          if (std::sscanf(line, "VmRSS: %ld kB",  &val) == 1) s.rss_kb   = val;
          if (std::sscanf(line, "VmPeak: %ld kB", &val) == 1) s.peak_kb  = val;
          if (std::sscanf(line, "VmSize: %ld kB", &val) == 1) s.vsize_kb = val;
      }
      std::fclose(f);
      return s;
  }
  #endif

  } // namespace

  void memory_overlay() {
  #ifdef TRACY_ENABLE
      ZoneScoped;
  #endif

      // Sample at most once per second
      const double now = ImGui::GetTime();
      if (s_last_sample < 0.0 || (now - s_last_sample) >= 1.0) {
          s_cached      = sample_memory();
          s_last_sample = now;
      }

      ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
                             | ImGuiWindowFlags_AlwaysAutoResize
                             | ImGuiWindowFlags_NoSavedSettings
                             | ImGuiWindowFlags_NoFocusOnAppearing
                             | ImGuiWindowFlags_NoNav
                             | ImGuiWindowFlags_NoMove;

      const float pad = 10.0f;
      const ImGuiViewport* vp = ImGui::GetMainViewport();
      ImVec2 pos(vp->WorkPos.x + vp->WorkSize.x - pad,
                 vp->WorkPos.y + vp->WorkSize.y - pad);
      ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 1.0f));
      ImGui::SetNextWindowBgAlpha(0.65f);

      if (ImGui::Begin("##memory_overlay", nullptr, flags)) {
          if (s_cached.rss_kb >= 0) {
              ImGui::Text("RSS:    %5ld MB", s_cached.rss_kb   / 1024);
              ImGui::Text("Peak:   %5ld MB", s_cached.peak_kb  / 1024);
  #ifndef _WIN32
              ImGui::Text("VSize:  %5ld MB", s_cached.vsize_kb / 1024);
  #endif
          } else {
              ImGui::TextDisabled("Memory: N/A");
          }
      }
      ImGui::End();
  }


id: plot_theme_cpp_core
name: plot_theme
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
tags: ["theme","colors","palette","styling","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui","implot"]
tests: []
file_path: cpp/functions/core/plot_theme.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: ccec236a278c13ca850773915133808c920c0a482636fd354e86e2161376e65a
signature:
  void plot_theme_dark() / void plot_theme_light() / void plot_theme_high_contrast() / void plot_theme_apply(const PlotTheme& theme)

description:
  Gestiona temas y paletas de colores para ImPlot e ImGui, con presets dark/light/high-contrast y soporte para temas custom

notes:
  - `plot_theme_apply` llama a `ImPlot::AddColormap` y `ImPlot::SetColormap` con el campo `name` del tema como identificador. Si se llama dos veces con el mismo `name`, ImPlot registra el colormap de nuevo — usar nombres distintos si se crean multiples variantes en runtime.
  - La funcion es "pure" en el sentido de que su unico efecto es escribir en `ImGuiStyle` e `ImPlotStyle` del contexto activo, sin I/O ni estado global propio.
  - Requiere que `ImGui::CreateContext()` e `ImPlot::CreateContext()` hayan sido llamados antes del primer uso.
  - Compatible con C++17. No usa excepciones ni RTTI.

documentation:
  # plot_theme

  Configura el aspecto visual de ventanas ImGui y graficas ImPlot en un solo paso. Tres presets listos para usar y una API de tema custom para casos especificos.

  ## Presets

  ### `plot_theme_dark()`

  Fondo oscuro (`#1A1A1F`) con paleta de 10 colores suaves: steel blue, soft green, warm orange, muted red, lavender, teal, gold, lime, pink, periwinkle. Ideal para dashboards de monitoreo.

  ### `plot_theme_light()`

  Fondo claro (`#F7F7F7`) con colores vibrantes de alto contraste sobre blanco. Recomendado para capturas de pantalla o exportacion a documentos.

  ### `plot_theme_high_contrast()`

  Fondo negro puro con colores neon: cyan, green, orange, magenta, yellow. Disenado para presentaciones en pantallas grandes o condiciones de poca luz.

  ## Uso

  ```cpp
  #include "plot_theme.h"

  // Al inicio del frame, antes de BeginMainMenuBar / Begin
  plot_theme_dark();

  // O con tema custom
  PlotTheme my_theme = plot_theme_preset_dark();
  my_theme.palette[0] = ImVec4(1.0f, 0.0f, 0.5f, 1.0f); // override primer color
  plot_theme_apply(my_theme);
  ```

  ## API

  ```cpp
  // Presets de aplicacion directa
  void plot_theme_dark();
  void plot_theme_light();
  void plot_theme_high_contrast();

  // Tema custom
  void plot_theme_apply(const PlotTheme& theme);

  // Obtener structs para inspeccion o modificacion parcial
  PlotTheme plot_theme_preset_dark();
  PlotTheme plot_theme_preset_light();
  PlotTheme plot_theme_preset_high_contrast();
  ```

  ## Estructura PlotTheme

  ```cpp
  struct PlotTheme {
      const char* name;       // nombre del colormap registrado en ImPlot
      ImVec4 bg;              // fondo del plot (ImPlotCol_PlotBg, ImGuiCol_WindowBg)
      ImVec4 frame_bg;        // fondo del frame (ImPlotCol_FrameBg, ImGuiCol_FrameBg)
      ImVec4 text;            // color del texto (todos los ImGui/ImPlot text cols)
      ImVec4 grid;            // lineas de cuadricula y ticks
      ImVec4 palette[10];     // paleta de colores para series de datos
      int palette_count;      // numero de colores activos en la paleta (1-10)
  };
  ```

code:
  #include "plot_theme.h"

  // ---------------------------------------------------------------------------
  // Preset structs
  // ---------------------------------------------------------------------------

  PlotTheme plot_theme_preset_dark() {
      PlotTheme t;
      t.name          = "dark";
      t.bg            = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
      t.frame_bg      = ImVec4(0.14f, 0.14f, 0.17f, 1.00f);
      t.text          = ImVec4(0.88f, 0.88f, 0.90f, 1.00f);
      t.grid          = ImVec4(0.30f, 0.30f, 0.35f, 0.60f);
      t.palette_count = 10;
      t.palette[0]    = ImVec4(0.33f, 0.62f, 0.91f, 1.00f); // steel blue
      t.palette[1]    = ImVec4(0.35f, 0.78f, 0.54f, 1.00f); // soft green
      t.palette[2]    = ImVec4(0.96f, 0.60f, 0.25f, 1.00f); // warm orange
      t.palette[3]    = ImVec4(0.85f, 0.38f, 0.42f, 1.00f); // muted red
      t.palette[4]    = ImVec4(0.72f, 0.52f, 0.88f, 1.00f); // lavender
      t.palette[5]    = ImVec4(0.38f, 0.80f, 0.82f, 1.00f); // teal
      t.palette[6]    = ImVec4(0.95f, 0.80f, 0.32f, 1.00f); // gold
      t.palette[7]    = ImVec4(0.60f, 0.82f, 0.38f, 1.00f); // lime
      t.palette[8]    = ImVec4(0.90f, 0.55f, 0.75f, 1.00f); // pink
      t.palette[9]    = ImVec4(0.55f, 0.70f, 0.95f, 1.00f); // periwinkle
      return t;
  }

  PlotTheme plot_theme_preset_light() {
      PlotTheme t;
      t.name          = "light";
      t.bg            = ImVec4(0.97f, 0.97f, 0.97f, 1.00f);
      t.frame_bg      = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
      t.text          = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
      t.grid          = ImVec4(0.70f, 0.70f, 0.72f, 0.60f);
      t.palette_count = 10;
      t.palette[0]    = ImVec4(0.12f, 0.47f, 0.71f, 1.00f); // deep blue
      t.palette[1]    = ImVec4(0.17f, 0.63f, 0.17f, 1.00f); // deep green
      t.palette[2]    = ImVec4(0.84f, 0.37f, 0.00f, 1.00f); // burnt orange
      t.palette[3]    = ImVec4(0.84f, 0.15f, 0.16f, 1.00f); // vivid red
      t.palette[4]    = ImVec4(0.58f, 0.40f, 0.74f, 1.00f); // purple
      t.palette[5]    = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // cyan
      t.palette[6]    = ImVec4(0.74f, 0.74f, 0.13f, 1.00f); // olive
      t.palette[7]    = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // sky blue
      t.palette[8]    = ImVec4(0.89f, 0.47f, 0.76f, 1.00f); // rose
      t.palette[9]    = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); // grey
      return t;
  }

  PlotTheme plot_theme_preset_high_contrast() {
      PlotTheme t;
      t.name          = "high_contrast";
      t.bg            = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
      t.frame_bg      = ImVec4(0.05f, 0.05f, 0.05f, 1.00f);
      t.text          = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
      t.grid          = ImVec4(0.30f, 0.30f, 0.30f, 0.80f);
      t.palette_count = 10;
      t.palette[0]    = ImVec4(0.00f, 1.00f, 1.00f, 1.00f); // cyan neon
      t.palette[1]    = ImVec4(0.00f, 1.00f, 0.00f, 1.00f); // green neon
      t.palette[2]    = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); // orange neon
      t.palette[3]    = ImVec4(1.00f, 0.00f, 0.50f, 1.00f); // magenta
      t.palette[4]    = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); // yellow
      t.palette[5]    = ImVec4(0.40f, 0.80f, 1.00f, 1.00f); // sky blue
      t.palette[6]    = ImVec4(1.00f, 0.40f, 0.40f, 1.00f); // salmon
      t.palette[7]    = ImVec4(0.60f, 1.00f, 0.40f, 1.00f); // lime
      t.palette[8]    = ImVec4(1.00f, 0.80f, 1.00f, 1.00f); // lavender bright
      t.palette[9]    = ImVec4(0.80f, 1.00f, 0.80f, 1.00f); // mint bright
      return t;
  }

  // ---------------------------------------------------------------------------
  // Internal helper
  // ---------------------------------------------------------------------------

  static void apply_imgui_colors(const PlotTheme& theme) {
      ImGuiStyle& s = ImGui::GetStyle();
      s.Colors[ImGuiCol_WindowBg]       = theme.bg;
      s.Colors[ImGuiCol_ChildBg]        = theme.bg;
      s.Colors[ImGuiCol_PopupBg]        = theme.frame_bg;
      s.Colors[ImGuiCol_FrameBg]        = theme.frame_bg;
      s.Colors[ImGuiCol_FrameBgHovered] = theme.frame_bg;
      s.Colors[ImGuiCol_FrameBgActive]  = theme.frame_bg;
      s.Colors[ImGuiCol_Text]           = theme.text;
      s.Colors[ImGuiCol_TextDisabled]   = ImVec4(theme.text.x * 0.5f,
                                                 theme.text.y * 0.5f,
                                                 theme.text.z * 0.5f,
                                                 0.80f);
  }

  static void apply_implot_colors(const PlotTheme& theme) {
      ImPlotStyle& ps = ImPlot::GetStyle();
      ps.Colors[ImPlotCol_PlotBg]       = theme.bg;
      ps.Colors[ImPlotCol_PlotBorder]   = theme.frame_bg;
      ps.Colors[ImPlotCol_FrameBg]      = theme.frame_bg;
      ps.Colors[ImPlotCol_LegendBg]     = theme.frame_bg;
      ps.Colors[ImPlotCol_LegendBorder] = theme.grid;
      ps.Colors[ImPlotCol_LegendText]   = theme.text;
      ps.Colors[ImPlotCol_TitleText]    = theme.text;
      ps.Colors[ImPlotCol_InlayText]    = theme.text;
      ps.Colors[ImPlotCol_AxisText]     = theme.text;
      ps.Colors[ImPlotCol_AxisGrid]     = theme.grid;
      ps.Colors[ImPlotCol_AxisTick]     = theme.grid;

      // Register custom colormap from palette
      const int count = theme.palette_count > 10 ? 10 : theme.palette_count;
      ImPlot::AddColormap(theme.name, theme.palette, count, false);
      ImPlot::SetColormap(theme.name);
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  void plot_theme_apply(const PlotTheme& theme) {
      apply_imgui_colors(theme);
      apply_implot_colors(theme);
  }

  void plot_theme_dark() {
      PlotTheme t = plot_theme_preset_dark();
      plot_theme_apply(t);
  }

  void plot_theme_light() {
      PlotTheme t = plot_theme_preset_light();
      plot_theme_apply(t);
  }

  void plot_theme_high_contrast() {
      PlotTheme t = plot_theme_preset_high_contrast();
      plot_theme_apply(t);
  }

params_schema:
  {"params":[{"name":"theme","desc":"Estructura PlotTheme con nombre, colores de fondo/frame/texto/grid y paleta de hasta 10 colores para series de datos"}],"output":"Aplica el tema al contexto ImPlot/ImGui actual modificando ImGuiStyle y ImPlotStyle en memoria"}


id: sidebar_cpp_core
name: sidebar
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: bool sidebar_begin(const char* title, bool* open, float width = 250.0f)
description: Panel lateral colapsable para filtros y controles de dashboard
tags: ["imgui","sidebar","panel","layout","dashboard","controls"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/sidebar.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 6d17e2d385a23082779e6593baa3f6806992f0657326d379bb873260164bdff5
example:
  ```cpp
  static bool filters_open = true;

  void render_fn() {
      if (sidebar_begin("Filters", &filters_open)) {
          ImGui::SliderFloat("Min value", &min_val, 0.0f, 100.0f);
          ImGui::Checkbox("Show inactive", &show_inactive);
      }
      sidebar_end();

      // contenido principal
      ImGui::Begin("Main");
      // ...
      ImGui::End();
  }
  ```

notes:
  El estado de `s_sidebar_was_open` es una variable estatica interna que coordina `sidebar_begin` y `sidebar_end`. Solo un sidebar activo a la vez por frame (patron begin/end clasico de ImGui). Para multiples sidebars simultaneos, instanciar logica propia con estado separado.

documentation:
  # sidebar

  Panel lateral colapsable. Cuando `*open == true` renderiza una ventana ImGui de ancho fijo con boton de cierre. Cuando `*open == false` muestra un boton compacto ">" para reabrir.

  Siempre llamar `sidebar_end()` despues de `sidebar_begin()`, independientemente del valor de retorno.

code:
  #include "core/sidebar.h"
  #include "imgui.h"

  static bool s_sidebar_was_open = false;

  bool sidebar_begin(const char* title, bool* open, float width) {
      if (!*open) {
          s_sidebar_was_open = false;
          if (ImGui::Button(">")) {
              *open = true;
          }
          return false;
      }

      s_sidebar_was_open = true;
      ImGui::SetNextWindowSize(ImVec2(width, 0.0f), ImGuiCond_Always);
      ImGui::Begin(title, open, ImGuiWindowFlags_NoCollapse);
      return true;
  }

  void sidebar_end() {
      if (s_sidebar_was_open) {
          ImGui::End();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del sidebar mostrado en la barra de titulo de la ventana"},{"name":"open","desc":"Puntero al estado abierto/cerrado; se pone a false si el usuario cierra la ventana"},{"name":"width","desc":"Ancho del sidebar en pixels (por defecto 250)"}],"output":"true si el sidebar esta abierto y se debe renderizar contenido entre sidebar_begin y sidebar_end"}


id: tab_container_cpp_core
name: tab_container
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: bool tab_container_begin(const char* id)
description: Contenedor de tabs para organizar vistas multiples en un dashboard
tags: ["imgui","tabs","container","layout","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/tab_container.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 4ab6bab6a6677ab5d3079bf8537e2fd9ae4ebeefe9b68520b57f5ad269c24003
example:
  ```cpp
  void render_fn() {
      ImGui::Begin("Dashboard");

      if (tab_container_begin("##main_tabs")) {
          if (tab_container_tab("Overview")) {
              render_overview();
              tab_container_tab_end();
          }
          if (tab_container_tab("Details")) {
              render_details();
              tab_container_tab_end();
          }
          if (tab_container_tab("Settings")) {
              render_settings();
              tab_container_tab_end();
          }
      }
      tab_container_end();

      ImGui::End();
  }
  ```

notes:
  Los flags `ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown` son fijos para estandarizar la experiencia de tabs en dashboards. Si se necesitan flags distintos usar `ImGui::BeginTabBar` directamente.

documentation:
  # tab_container

  Wrapper fino sobre `ImGui::BeginTabBar` / `EndTabBar` con flags de dashboard preconfigurados: `Reorderable` (arrastrar tabs) y `FittingPolicyResizeDown` (tabs se achican si no caben).

  API de cuatro funciones siguiendo el patron begin/end de ImGui. Siempre llamar `tab_container_end()` si `tab_container_begin()` retorno true. Siempre llamar `tab_container_tab_end()` dentro del bloque `if (tab_container_tab(...))`.

code:
  #include "core/tab_container.h"
  #include "imgui.h"

  bool tab_container_begin(const char* id) {
      return ImGui::BeginTabBar(
          id,
          ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown
      );
  }

  bool tab_container_tab(const char* label) {
      return ImGui::BeginTabItem(label);
  }

  void tab_container_tab_end() {
      ImGui::EndTabItem();
  }

  void tab_container_end() {
      ImGui::EndTabBar();
  }

params_schema:
  {"params":[{"name":"id","desc":"Identificador unico del tab bar (ej: '##views'). Puede ser invisible con prefijo ##"},{"name":"label","desc":"Etiqueta visible de cada tab individual"}],"output":"tab_container_begin: true si el tab bar esta activo. tab_container_tab: true si el tab esta seleccionado y se debe renderizar su contenido"}


id: time_series_buffer_cpp_core
name: time_series_buffer
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: TimeSeriesBuffer(size_t capacity)
tags: ["buffer","timeseries","streaming","dashboard","data"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: cpp/functions/core/time_series_buffer.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: bf82ceb467c8e2bf492d9b9805f3562c7f7ab08c358c9babe336335522726c3d
description:
  Ring buffer circular para datos de series temporales, optimizado para streaming de metricas en dashboards en tiempo real

notes:
  - No depende de ImGui ni ImPlot directamente — es una estructura de datos pura.
  - No es thread-safe. Para uso multihilo, proteger con mutex externo.
  - Move semantics implementadas; copy queda eliminada (`= delete`) para evitar copias accidentales del heap.
  - `min`, `max` y `average` iteran sobre `count` elementos — O(n). Para buffers grandes en hot paths, considerar mantener un acumulador incremental.

documentation:
  # time_series_buffer

  Ring buffer circular de floats para series temporales en tiempo real. Diseñado para alimentar plots de ImPlot con metricas de streaming (FPS, latencia, uso de CPU, etc.).

  ## Uso basico

  ```cpp
  TimeSeriesBuffer fps_history(512);

  // En el loop principal:
  fps_history.push(ImGui::GetIO().Framerate);

  // Para renderizar con ImPlot:
  float ordered[512];
  size_t n = fps_history.copy_ordered(ordered, 512);
  ImPlot::PlotLines("FPS", ordered, (int)n);
  ```

  ## Semantica del ring buffer

  - `push(v)` escribe en `data[offset % capacity]` y avanza el write head.
  - `count` crece hasta `capacity` y se mantiene ahi — el buffer nunca desborda.
  - `get(0)` retorna el elemento mas antiguo; `get(count-1)` el mas reciente.
  - `latest()` es equivalente a `get(count-1)` pero mas explicito.
  - `copy_ordered` extrae los datos en orden cronologico para pasarlos a ImPlot u otro consumidor que espere un array contiguo.

code:
  #include "time_series_buffer.h"

  #include <algorithm>
  #include <cassert>
  #include <cstring>
  #include <limits>
  #include <numeric>
  #include <utility>

  TimeSeriesBuffer::TimeSeriesBuffer(size_t cap)
      : data(new float[cap]), capacity(cap), count(0), offset(0) {}

  TimeSeriesBuffer::~TimeSeriesBuffer() {
      delete[] data;
  }

  TimeSeriesBuffer::TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept
      : data(other.data), capacity(other.capacity), count(other.count), offset(other.offset) {
      other.data     = nullptr;
      other.capacity = 0;
      other.count    = 0;
      other.offset   = 0;
  }

  TimeSeriesBuffer& TimeSeriesBuffer::operator=(TimeSeriesBuffer&& other) noexcept {
      if (this != &other) {
          delete[] data;
          data           = other.data;
          capacity       = other.capacity;
          count          = other.count;
          offset         = other.offset;
          other.data     = nullptr;
          other.capacity = 0;
          other.count    = 0;
          other.offset   = 0;
      }
      return *this;
  }

  void TimeSeriesBuffer::push(float value) {
      data[offset % capacity] = value;
      offset++;
      if (count < capacity) count++;
  }

  float TimeSeriesBuffer::get(size_t index) const {
      assert(index < count);
      // oldest element is at (offset - count) % capacity
      size_t real = (offset - count + index) % capacity;
      return data[real];
  }

  float TimeSeriesBuffer::latest() const {
      assert(count > 0);
      return data[(offset - 1) % capacity];
  }

  float TimeSeriesBuffer::min() const {
      float m = std::numeric_limits<float>::max();
      for (size_t i = 0; i < count; ++i) m = std::min(m, get(i));
      return m;
  }

  float TimeSeriesBuffer::max() const {
      float m = std::numeric_limits<float>::lowest();
      for (size_t i = 0; i < count; ++i) m = std::max(m, get(i));
      return m;
  }

  float TimeSeriesBuffer::average() const {
      if (count == 0) return 0.0f;
      float sum = 0.0f;
      for (size_t i = 0; i < count; ++i) sum += get(i);
      return sum / static_cast<float>(count);
  }

  size_t TimeSeriesBuffer::size() const {
      return count;
  }

  bool TimeSeriesBuffer::full() const {
      return count == capacity;
  }

  size_t TimeSeriesBuffer::copy_ordered(float* out, size_t out_capacity) const {
      size_t n = std::min(count, out_capacity);
      for (size_t i = 0; i < n; ++i)
          out[i] = get(i);
      return n;
  }

params_schema:
  {"params":[{"name":"capacity","desc":"Numero maximo de muestras que almacena el buffer"}],"output":"Buffer circular listo para push de datos y lectura ordenada"}


id: tracy_zone_cpp_core
name: tracy_zone
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: FN_ZONE(name) / FN_ZONE_COLOR(name, color) / FN_FRAME_MARK / FN_PLOT(name, val)
description: Macros y constantes de conveniencia para Tracy profiling zones, compilables sin Tracy
tags: ["tracy","profiling","debug","performance","raii"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: cpp/functions/core/tracy_zone.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: d97e1d27b60810d0c0ae4455f1e85d22d3ffa3b25cd2927535c48a76ffc12c91
example:
  ```cpp
  #include "core/tracy_zone.h"

  void render_scene() {
      FN_ZONE_COLOR("render_scene", fn_tracy::COLOR_RENDER);
      // ... render calls ...
  }

  void update(float dt) {
      FN_ZONE("update");
      // ... game logic ...
      FN_PLOT("dt_ms", dt * 1000.0f);
  }

  void main_loop() {
      while (running) {
          update(dt);
          render_scene();
          FN_FRAME_MARK;
      }
  }
  ```

notes:
  - Compilar con `-DTRACY_ENABLE` y enlazar `TracyClient.cpp` para activar profiling.
  - Sin `TRACY_ENABLE` el .cpp es prácticamente vacío — coste cero en producción.
  - Los colores son ARGB; si el alpha es 0 Tracy aplica su color automático por zona.
  - `FN_ZONE` expande a `ZoneScopedN(name)` de Tracy, que crea el contexto con `__LINE__` como discriminador — es seguro llamar varias veces en el mismo scope.

documentation:
  # tracy_zone

  Macros de scope para instrumentar secciones de código con Tracy Profiler. Cuando `TRACY_ENABLE` no está definido (ej. builds de producción) todas las macros se expanden a `(void)0`, sin coste alguno.

  ## Macros disponibles

  | Macro | Descripción |
  |---|---|
  | `FN_ZONE("name")` | Zona con nombre, color automático |
  | `FN_ZONE_COLOR("name", color)` | Zona con nombre y color ARGB explícito |
  | `FN_FRAME_MARK` | Marca el límite de frame (eje X de Tracy) |
  | `FN_PLOT("name", val)` | Envía un valor escalar al panel de plots |

  ## Colores predefinidos (`fn_tracy::`)

  ```cpp
  fn_tracy::COLOR_RENDER  // 0x2196F3 azul   — rendering
  fn_tracy::COLOR_UPDATE  // 0x4CAF50 verde  — game update / logic
  fn_tracy::COLOR_IO      // 0xFF9800 naranja — I/O disco/red
  fn_tracy::COLOR_NETWORK // 0xF44336 rojo   — red/HTTP
  fn_tracy::COLOR_COMPUTE // 0x9C27B0 morado — compute / shader prep
  ```

code:
  // tracy_zone.cpp
  // All definitions live in tracy_zone.h (macros + constexpr).
  // This translation unit exists so the header can be included in any build
  // without requiring Tracy — compile with -DTRACY_ENABLE to activate profiling.
  #include "core/tracy_zone.h"

params_schema:
  {"params":[{"name":"name","desc":"Nombre de la zona tal como aparece en la timeline de Tracy"},{"name":"color","desc":"Color ARGB uint32 de la zona en Tracy (opcional, solo FN_ZONE_COLOR)"}],"output":"Zona Tracy activa durante el scope actual; no-op cuando TRACY_ENABLE no está definido"}


id: bar_chart_cpp_viz
name: bar_chart
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: Renderiza un grafico de barras verticales usando ImPlot dentro de un frame ImGui
tags: ["implot","chart","visualization","gpu","bar"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot"]
tests: []
file_path: cpp/functions/viz/bar_chart.cpp
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
framework: imgui
variant: []
content_hash: f4b83230686eb4031c0bd4fcc3851aee0475dcc7e8baa1297e759c9356783d48
signature:
  void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width)

documentation:
  # bar_chart

  Wrapper atomico sobre `ImPlot::PlotBars` con configuracion automatica de etiquetas en el eje X.

  Debe llamarse dentro del render callback de `fn::run_app`.

code:
  #include "viz/bar_chart.h"
  #include "implot.h"

  #include <vector>

  void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          std::vector<double> positions(count);
          for (int i = 0; i < count; i++) positions[i] = i;

          ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels);
          ImPlot::PlotBars("##data", values, count, bar_width);
          ImPlot::EndPlot();
      }
  }

  void bar_chart(const char* title, const char* const* labels, const double* values, int count, double bar_width) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          std::vector<double> positions(count);
          for (int i = 0; i < count; i++) positions[i] = i;

          ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels);
          ImPlot::PlotBars("##data", values, count, bar_width);
          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del grafico de barras"},{"name":"labels","desc":"Array de etiquetas para el eje X, una por barra"},{"name":"values","desc":"Array de valores numericos para la altura de cada barra"},{"name":"count","desc":"Numero de barras (longitud de labels y values)"},{"name":"bar_width","desc":"Ancho de cada barra como fraccion del espacio disponible (default 0.67)"}],"output":"Renderiza el grafico de barras en el frame ImGui actual"}


id: candlestick_cpp_viz
name: candlestick
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
tags: ["implot","chart","visualization","gpu","candlestick","ohlc","finance"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot","imgui"]
tests: []
file_path: cpp/functions/viz/candlestick.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: c21c6466b2b06e80e69f72790865b6f012153a8665c1da016f8ac97a6e5897d0
signature:
  void candlestick(const char* title, const double* dates, const double* opens, const double* closes, const double* lows, const double* highs, int count, float width_percent = 0.25f, bool tooltip = true)

description:
  Renderiza un grafico de velas OHLC usando ImPlot custom rendering. Verde para velas alcistas (close >= open), rojo para bajistas.

example:
  ```cpp
  // arrays de datos financieros (timestamps Unix, precios)
  candlestick("BTC/USD", dates, opens, closes, lows, highs, 90);

  // sin tooltip, velas mas anchas
  candlestick("ETH/USD", dates, opens, closes, lows, highs, 30, 0.6f, false);
  ```

notes:
  - El ancho de cada vela se calcula como `(dates[1] - dates[0]) * width_percent * 0.5` en cada lado. Asume spacing uniforme entre velas.
  - Para un solo punto (`count == 1`) el spacing por defecto es 1.0.
  - La deteccion de hover usa un margen de 4px alrededor del area cuerpo+wick para facilitar la interaccion.
  - El eje X usa `ImPlotScale_Time` — si los datos son indices numericos simples (0, 1, 2...) en lugar de timestamps, pasar `ImPlotAxisFlags_NoDecorations` o cambiar `SetupAxisScale`.

documentation:
  # candlestick

  Grafico de velas OHLC completo usando custom rendering de ImPlot. Dibuja body (open-close) y wicks (high-low) por vela usando `ImPlot::GetPlotDrawList()` y `ImPlot::PlotToPixels()` para conversion de coordenadas.

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). El eje X se configura con `ImPlotScale_Time` para timestamps Unix.

  Solo tiene overload `double` porque los datos financieros requieren doble precision.

code:
  #include "viz/candlestick.h"
  #include "imgui.h"
  #include "implot.h"

  void candlestick(const char* title, const double* dates, const double* opens,
                   const double* closes, const double* lows, const double* highs,
                   int count, float width_percent, bool tooltip) {
      if (count <= 0) return;

      // Compute half-width of each candle body in data coordinates.
      // Use spacing between consecutive dates when count > 1, else fallback to 0.5.
      double spacing = (count > 1) ? (dates[1] - dates[0]) : 1.0;
      double half_w  = spacing * (double)width_percent * 0.5;

      ImPlot::SetupAxes("Date", "Price", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
      ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time);

      // Auto-fit X axis to the data range with a small margin.
      ImPlot::SetupAxisLimits(ImAxis_X1,
                              dates[0]         - spacing,
                              dates[count - 1] + spacing,
                              ImGuiCond_Always);

      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          ImDrawList* draw = ImPlot::GetPlotDrawList();

          const ImU32 col_bull = IM_COL32(0,  200, 80,  255);  // green — close >= open
          const ImU32 col_bear = IM_COL32(220, 50, 50,  255);  // red   — close < open

          int hovered_idx = -1;

          for (int i = 0; i < count; i++) {
              double x     = dates[i];
              double open  = opens[i];
              double close = closes[i];
              double low   = lows[i];
              double high  = highs[i];

              bool bullish = (close >= open);
              ImU32 col   = bullish ? col_bull : col_bear;

              // Convert data coordinates to screen pixels.
              ImVec2 body_tl = ImPlot::PlotToPixels(x - half_w, bullish ? close : open);
              ImVec2 body_br = ImPlot::PlotToPixels(x + half_w, bullish ? open  : close);
              ImVec2 wick_hi = ImPlot::PlotToPixels(x, high);
              ImVec2 wick_lo = ImPlot::PlotToPixels(x, low);
              float  cx      = (body_tl.x + body_br.x) * 0.5f;

              // Wick (high-low vertical line).
              draw->AddLine(ImVec2(cx, wick_hi.y), ImVec2(cx, wick_lo.y), col, 1.5f);

              // Body rectangle (open-close).
              // Ensure at least 1px height so flat candles are visible.
              if (body_br.y <= body_tl.y + 1.0f) body_br.y = body_tl.y + 1.0f;
              draw->AddRectFilled(body_tl, body_br, col);
              draw->AddRect(body_tl, body_br, col);

              // Track hovered candle for tooltip.
              if (tooltip && ImPlot::IsPlotHovered()) {
                  ImVec2 mouse = ImGui::GetMousePos();
                  if (mouse.x >= body_tl.x - 4 && mouse.x <= body_br.x + 4 &&
                      mouse.y >= wick_hi.y  - 4 && mouse.y <= wick_lo.y  + 4) {
                      hovered_idx = i;
                  }
              }
          }

          // Tooltip for the hovered candle.
          if (tooltip && hovered_idx >= 0) {
              int i = hovered_idx;
              ImGui::BeginTooltip();
              ImGui::Text("O: %.4f", opens[i]);
              ImGui::Text("H: %.4f", highs[i]);
              ImGui::Text("L: %.4f", lows[i]);
              ImGui::Text("C: %.4f", closes[i]);
              ImGui::EndTooltip();
          }

          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del grafico, se muestra como header del plot"},{"name":"dates","desc":"Array de timestamps Unix o indices numericos del eje X, uno por vela"},{"name":"opens","desc":"Array de precios de apertura, uno por vela"},{"name":"closes","desc":"Array de precios de cierre, uno por vela"},{"name":"lows","desc":"Array de precios minimos (punta inferior del wick), uno por vela"},{"name":"highs","desc":"Array de precios maximos (punta superior del wick), uno por vela"},{"name":"count","desc":"Numero de velas (longitud de todos los arrays)"},{"name":"width_percent","desc":"Ancho del body de cada vela como fraccion del espacio entre puntos consecutivos (0.0-1.0, default 0.25)"},{"name":"tooltip","desc":"Si true, muestra tooltip con valores O/H/L/C al hacer hover sobre una vela"}],"output":"Renderiza el grafico de velas OHLC en el frame ImGui actual, sin retornar valor"}


id: gauge_cpp_viz
name: gauge
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
signature: void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f)
description: Renderiza un indicador circular tipo gauge/velocimetro usando ImGui draw primitives
tags: ["imgui","visualization","gauge","kpi","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/gauge.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 585a6c8133bbb20d80773f10639191b4fbb118ac7496940a7745f3e06b01b86a
example:
  ```cpp
  // KPI card con gauge de temperatura
  gauge("CPU Temp", 72.5f, 0.0f, 100.0f, 50.0f);

  // Gauge grande para dashboard principal
  gauge("Velocidad", 3200.0f, 0.0f, 5000.0f, 80.0f);
  ```

notes:
  - El arco de fondo es gris oscuro (IM_COL32(60,60,60,220)), 6px de grosor.
  - La aguja tiene longitud del 75% del radio para evitar solapar el arco.
  - Usa solo `float`; no ofrece overload `double` porque ImGui DrawList trabaja en coordenadas de pantalla (float).
  - El espacio reservado es `diameter x (diameter + line_height)` para incluir la etiqueta.

documentation:
  # gauge

  Indicador circular tipo gauge/velocimetro construido sobre ImGui draw primitives. No requiere ImPlot.

  El arco ocupa 240 grados (de 150deg a 390deg en sentido horario). El color del arco de valor interpolado de verde (minimo) a amarillo (mitad) a rojo (maximo). Una aguja blanca apunta al valor actual.

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).

code:
  #include "viz/gauge.h"
  #include "imgui.h"
  #include <cmath>
  #include <cstdio>

  #ifndef M_PI
  #define M_PI 3.14159265358979323846f
  #endif

  void gauge(const char* label, float value, float min_val, float max_val, float radius) {
      ImDrawList* draw_list = ImGui::GetWindowDrawList();
      ImVec2 pos = ImGui::GetCursorScreenPos();

      // Reserve space: diameter + label line
      float diameter = radius * 2.0f;
      ImGui::Dummy(ImVec2(diameter, diameter + ImGui::GetTextLineHeightWithSpacing()));

      ImVec2 center = ImVec2(pos.x + radius, pos.y + radius);

      // Arc spans 240 degrees: from 150deg to 390deg (i.e. 150 to 30 going clockwise)
      // In screen space Y is down, so angles go clockwise.
      // Start angle: 150 degrees = bottom-left, End angle: 390 = 30 degrees = bottom-right
      const float angle_start = (150.0f * (float)M_PI) / 180.0f;
      const float angle_end   = (390.0f * (float)M_PI) / 180.0f;
      const int   num_segments = 64;

      // Background arc (dark gray)
      ImU32 bg_color = IM_COL32(60, 60, 60, 220);
      for (int i = 0; i < num_segments; i++) {
          float a0 = angle_start + (angle_end - angle_start) * ((float)i       / num_segments);
          float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
          draw_list->AddLine(
              ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
              ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
              bg_color, 6.0f);
      }

      // Normalize value to [0, 1]
      float t = 0.0f;
      if (max_val > min_val) {
          t = (value - min_val) / (max_val - min_val);
          if (t < 0.0f) t = 0.0f;
          if (t > 1.0f) t = 1.0f;
      }

      // Color: green (t=0) -> yellow (t=0.5) -> red (t=1)
      float r, g, b;
      if (t < 0.5f) {
          float s = t * 2.0f;
          r = (unsigned char)(s * 255);
          g = 200;
          b = 0;
      } else {
          float s = (t - 0.5f) * 2.0f;
          r = 220;
          g = (unsigned char)((1.0f - s) * 200);
          b = 0;
      }
      ImU32 value_color = IM_COL32((int)r, (int)g, (int)b, 255);

      // Value arc
      float angle_value = angle_start + (angle_end - angle_start) * t;
      int value_segments = (int)(num_segments * t);
      for (int i = 0; i < value_segments; i++) {
          float a0 = angle_start + (angle_end - angle_start) * ((float)i       / num_segments);
          float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
          draw_list->AddLine(
              ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
              ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
              value_color, 6.0f);
      }

      // Needle: line from center to arc at current angle
      float needle_len = radius * 0.75f;
      ImVec2 needle_tip = ImVec2(
          center.x + cosf(angle_value) * needle_len,
          center.y + sinf(angle_value) * needle_len);
      draw_list->AddLine(center, needle_tip, IM_COL32(255, 255, 255, 240), 2.0f);
      draw_list->AddCircleFilled(center, 4.0f, IM_COL32(255, 255, 255, 200));

      // Value text centered
      char val_buf[32];
      snprintf(val_buf, sizeof(val_buf), "%.1f", value);
      ImVec2 val_size = ImGui::CalcTextSize(val_buf);
      draw_list->AddText(
          ImVec2(center.x - val_size.x * 0.5f, center.y + radius * 0.35f),
          IM_COL32(230, 230, 230, 255), val_buf);

      // Label below value
      ImVec2 label_size = ImGui::CalcTextSize(label);
      draw_list->AddText(
          ImVec2(center.x - label_size.x * 0.5f, center.y + radius * 0.35f + val_size.y + 2.0f),
          IM_COL32(180, 180, 180, 200), label);
  }

params_schema:
  {"params":[{"name":"label","desc":"Etiqueta del gauge, se muestra centrada debajo del valor numerico"},{"name":"value","desc":"Valor actual a mostrar en el gauge"},{"name":"min_val","desc":"Valor minimo de la escala (extremo izquierdo del arco)"},{"name":"max_val","desc":"Valor maximo de la escala (extremo derecho del arco)"},{"name":"radius","desc":"Radio del gauge en pixels (default 60.0)"}],"output":"Renderiza el gauge en el frame ImGui actual, reservando espacio con ImGui::Dummy"}


id: graph_force_layout_cpp_viz
name: graph_force_layout
kind: function
lang: cpp
domain: viz
version: 1.0.0
purity: pure
signature: float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
tags: ["graph","layout","force-directed","barnes-hut","physics","gpu"]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
imports: []
tests: []
file_path: cpp/functions/viz/graph_force_layout.cpp
created_at: 2026-04-07T22:05:15Z
updated_at: 2026-04-07T22:05:15Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 9311ea971afaf855d46541d53f54f62c1e6e92f8232d28eee5deb1fd506dff4f
description:
  Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada

example:
  ```cpp
  static ForceLayoutConfig cfg;
  static bool running = true;

  if (running) {
      float energy = graph_force_layout_step(my_graph, cfg);
      if (energy < 0.01f) running = false; // convergido
  }
  ```

notes:
  - El quadtree usa un pool estatico de `1 << 20` (~1M) celdas. Para grafos de >500K nodos
    se recomienda reducir `MAX_QUAD_NODES` o aumentarlo segun memoria disponible.
  - La pila de traversal en `quad_force` es tambien estatica (`static int stack[]`); no es
    thread-safe si se llama desde multiples hilos simultaneamente.
  - `graph_force_layout_reset` usa `rand()`. Para reproducibilidad llama `srand(seed)` antes.
  - Los buffers de fuerza (`fx_buf`, `fy_buf`) se realocan una sola vez cuando el conteo de
    nodos supera la capacidad previa; en el uso normal (tamano fijo) no hay allocaciones
    por frame.

documentation:
  # graph_force_layout

  Implementa el algoritmo de layout force-directed clasico (Fruchterman-Reingold / Eades) con aproximacion Barnes-Hut O(n log n) para escalar a grafos de miles de nodos.

  ## Algoritmo

  Cada llamada a `graph_force_layout_step` ejecuta `config.iterations` pasos. Un paso:

  1. **Construccion del quadtree** (Barnes-Hut): se calcula el bounding box de las posiciones actuales, se construye un quadtree flat en `quad_pool` (sin allocaciones por nodo). Cada celda acumula centro de masa y masa total.
  2. **Repulsion**: para cada nodo se recorre el quadtree. Si el cociente `cell_size / distance < theta`, la celda se trata como una sola masa puntual (multipolo de orden 0). Si no, se desciende a los hijos. Con `theta=0` es O(n²) exacto; con `theta=0.5` es O(n log n).
  3. **Atraccion**: para cada arista `(s, t)`, fuerza de Hooke `F = k * dist * weight` en la direccion del arco.
  4. **Gravedad**: fuerza proporcional a la distancia al origen, evita que el grafo derive fuera de pantalla.
  5. **Integracion**: `v = v * damping + F`, `pos += v`, con clamping de velocidad.
  6. Nodos con `pinned = true` no se mueven en ningun paso.

  ## Funciones auxiliares

  ```cpp
  // Randomizar posiciones para empezar la simulacion
  graph_force_layout_reset(graph, 200.0f);

  // Layout circular instantaneo (sin iteracion)
  graph_layout_circular(graph, 150.0f);

  // Layout en grid instantaneo
  graph_layout_grid(graph, 25.0f);
  ```

code:
  #include "viz/graph_force_layout.h"
  #include "viz/graph_types.h"

  #include <cmath>
  #include <cstdlib>
  #include <algorithm>

  // ---------------------------------------------------------------------------
  // Quadtree for Barnes-Hut approximation
  // ---------------------------------------------------------------------------

  struct QuadNode {
      float cx, cy;       // center of mass
      float mass;         // total mass (node count in subtree)
      float x0, y0;       // bounding box min
      float x1, y1;       // bounding box max
      int children[4];    // NW=0, NE=1, SW=2, SE=3  (-1 = empty)
      int body;           // node index if leaf (-1 if internal)
  };

  static constexpr int MAX_QUAD_NODES = 1 << 20; // supports graphs up to ~1M nodes
  static QuadNode quad_pool[MAX_QUAD_NODES];
  static int quad_count = 0;

  static int quad_new(float x0, float y0, float x1, float y1) {
      if (quad_count >= MAX_QUAD_NODES) return -1;
      int idx = quad_count++;
      QuadNode& q = quad_pool[idx];
      q.cx = 0; q.cy = 0; q.mass = 0;
      q.x0 = x0; q.y0 = y0; q.x1 = x1; q.y1 = y1;
      q.children[0] = q.children[1] = q.children[2] = q.children[3] = -1;
      q.body = -1;
      return idx;
  }

  // Determine quadrant index for point (px,py) relative to cell midpoint.
  // 0=NW, 1=NE, 2=SW, 3=SE
  static int quad_child_idx(const QuadNode& q, float px, float py) {
      float mx = (q.x0 + q.x1) * 0.5f;
      float my = (q.y0 + q.y1) * 0.5f;
      int xi = (px >= mx) ? 1 : 0;
      int yi = (py >= my) ? 2 : 0;
      return xi | yi;
  }

  // Subdivide cell qi into four children.
  static void quad_subdivide(int qi) {
      QuadNode& q = quad_pool[qi];
      float mx = (q.x0 + q.x1) * 0.5f;
      float my = (q.y0 + q.y1) * 0.5f;
      // NW
      quad_pool[qi].children[0] = quad_new(q.x0, q.y0, mx, my);
      // NE
      quad_pool[qi].children[1] = quad_new(mx, q.y0, q.x1, my);
      // SW
      quad_pool[qi].children[2] = quad_new(q.x0, my, mx, q.y1);
      // SE
      quad_pool[qi].children[3] = quad_new(mx, my, q.x1, q.y1);
  }

  // Insert body (node_idx at position nx,ny with mass nmass) into cell qi.
  // Uses iterative descent to avoid stack overflow on deep trees.
  static void quad_insert(int root, int node_idx, float nx, float ny, float nmass) {
      int qi = root;
      while (qi >= 0) {
          QuadNode& q = quad_pool[qi];
          // Update center of mass
          float total = q.mass + nmass;
          q.cx = (q.cx * q.mass + nx * nmass) / total;
          q.cy = (q.cy * q.mass + ny * nmass) / total;
          q.mass = total;

          if (q.body == -1 && q.children[0] == -1) {
              // Empty leaf: place body here
              q.body = node_idx;
              return;
          }

          if (q.body >= 0) {
              // Leaf with existing body: subdivide, push existing body down
              quad_subdivide(qi);
              // Move old body into correct child (re-read q after subdivide since pool may shift)
              QuadNode& qq = quad_pool[qi];
              int old_body = qq.body;
              float obx = /* we need positions */ 0, oby = 0;
              // We store positions in the GraphData, pass via closure is not possible here.
              // Instead we pass a pointer to positions alongside. We'll fix this by using
              // a file-scope pointer set before each build.
              (void)old_body; (void)obx; (void)oby;
              // NOTE: positions accessed via file-scope g_nodes pointer below.
              qq.body = -1;
          }

          int ci = quad_child_idx(quad_pool[qi], nx, ny);
          qi = quad_pool[qi].children[ci];
      }
  }

  // File-scope pointers set before each tree build (avoids passing them everywhere).
  static const GraphNode* g_nodes = nullptr;

  // Insert body knowing positions from g_nodes.
  static void quad_insert_body(int qi, int node_idx) {
      float nx = g_nodes[node_idx].x;
      float ny = g_nodes[node_idx].y;
      const float nmass = 1.0f;

      while (qi >= 0) {
          QuadNode& q = quad_pool[qi];
          float total = q.mass + nmass;
          q.cx = (q.cx * q.mass + nx * nmass) / total;
          q.cy = (q.cy * q.mass + ny * nmass) / total;
          q.mass = total;

          if (q.body == -1 && q.children[0] == -1) {
              // Empty leaf
              q.body = node_idx;
              return;
          }

          if (q.children[0] == -1) {
              // Leaf occupied: subdivide and push existing body down
              int old_body = q.body;
              q.body = -1;
              quad_subdivide(qi);

              // Push old body into child
              int old_ci = quad_child_idx(quad_pool[qi], g_nodes[old_body].x, g_nodes[old_body].y);
              int old_child = quad_pool[qi].children[old_ci];
              if (old_child >= 0) {
                  QuadNode& oc = quad_pool[old_child];
                  oc.cx = g_nodes[old_body].x;
                  oc.cy = g_nodes[old_body].y;
                  oc.mass = 1.0f;
                  oc.body = old_body;
              }
          }

          int ci = quad_child_idx(quad_pool[qi], nx, ny);
          qi = quad_pool[qi].children[ci];
      }
  }

  // Compute Barnes-Hut repulsion force on node at (nx,ny) from subtree qi.
  // Accumulates force into (fx, fy).
  static void quad_force(int qi, float nx, float ny,
                         float theta, float repulsion, float min_dist,
                         float& fx, float& fy) {
      // Iterative traversal using a small stack to avoid recursion depth issues.
      static int stack[MAX_QUAD_NODES]; // reuse static stack
      int top = 0;
      stack[top++] = qi;

      while (top > 0) {
          int ci = stack[--top];
          if (ci < 0) continue;
          const QuadNode& q = quad_pool[ci];
          if (q.mass == 0) continue;

          float dx = q.cx - nx;
          float dy = q.cy - ny;
          float dist2 = dx * dx + dy * dy;
          float dist = std::sqrt(dist2);
          if (dist < min_dist) dist = min_dist;

          // Cell size
          float cell_size = q.x1 - q.x0;

          // Use multipole approximation if far enough OR if leaf
          bool is_leaf = (q.children[0] == -1);
          if (is_leaf || (cell_size / dist) < theta) {
              // Coulomb repulsion: F = repulsion * mass / dist^2
              float force = repulsion * q.mass / (dist * dist);
              fx -= force * dx / dist;
              fy -= force * dy / dist;
          } else {
              // Push children
              for (int k = 0; k < 4; ++k) {
                  if (q.children[k] >= 0)
                      stack[top++] = q.children[k];
              }
          }
      }
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config) {
      if (graph.node_count <= 0) return 0.0f;

      // Temporary force accumulators (stack-allocated for small graphs, static for large)
      static float* fx_buf = nullptr;
      static float* fy_buf = nullptr;
      static int    buf_cap = 0;

      if (graph.node_count > buf_cap) {
          delete[] fx_buf;
          delete[] fy_buf;
          buf_cap = graph.node_count + 64;
          fx_buf = new float[buf_cap];
          fy_buf = new float[buf_cap];
      }

      float total_energy = 0.0f;

      for (int iter = 0; iter < config.iterations; ++iter) {
          // Zero forces
          for (int i = 0; i < graph.node_count; ++i) {
              fx_buf[i] = 0.0f;
              fy_buf[i] = 0.0f;
          }

          // ---- Build Barnes-Hut quadtree ----
          // Compute bounding box of current positions
          float bx0 = graph.nodes[0].x, bx1 = graph.nodes[0].x;
          float by0 = graph.nodes[0].y, by1 = graph.nodes[0].y;
          for (int i = 1; i < graph.node_count; ++i) {
              float px = graph.nodes[i].x, py = graph.nodes[i].y;
              if (px < bx0) bx0 = px; if (px > bx1) bx1 = px;
              if (py < by0) by0 = py; if (py > by1) by1 = py;
          }
          // Add margin to avoid degeneracies
          float margin = (bx1 - bx0 + by1 - by0) * 0.05f + 1.0f;
          bx0 -= margin; bx1 += margin;
          by0 -= margin; by1 += margin;
          // Make it square
          float side = std::max(bx1 - bx0, by1 - by0);
          float cx = (bx0 + bx1) * 0.5f, cy = (by0 + by1) * 0.5f;
          bx0 = cx - side * 0.5f; bx1 = cx + side * 0.5f;
          by0 = cy - side * 0.5f; by1 = cy + side * 0.5f;

          quad_count = 0;
          g_nodes = graph.nodes;
          int root = quad_new(bx0, by0, bx1, by1);

          for (int i = 0; i < graph.node_count; ++i) {
              quad_insert_body(root, i);
          }

          // ---- Repulsion via Barnes-Hut ----
          for (int i = 0; i < graph.node_count; ++i) {
              if (graph.nodes[i].pinned) continue;
              quad_force(root,
                         graph.nodes[i].x, graph.nodes[i].y,
                         config.theta, config.repulsion, config.min_distance,
                         fx_buf[i], fy_buf[i]);
              // Subtract self-interaction (the tree includes the node itself)
              // Self-force: repulsion * 1 / min_dist^2, but direction is (0,0) -> skip
          }

          // ---- Attraction along edges (spring force) ----
          for (int e = 0; e < graph.edge_count; ++e) {
              const GraphEdge& edge = graph.edges[e];
              int s = (int)edge.source;
              int t = (int)edge.target;
              if (s < 0 || s >= graph.node_count) continue;
              if (t < 0 || t >= graph.node_count) continue;

              float dx = graph.nodes[t].x - graph.nodes[s].x;
              float dy = graph.nodes[t].y - graph.nodes[s].y;
              float dist = std::sqrt(dx * dx + dy * dy);
              if (dist < config.min_distance) dist = config.min_distance;

              // F = k * dist * weight  (Hooke: pulls toward equilibrium at 0)
              float force = config.attraction * dist * edge.weight;
              float fx_e = force * dx / dist;
              float fy_e = force * dy / dist;

              if (!graph.nodes[s].pinned) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; }
              if (!graph.nodes[t].pinned) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; }
          }

          // ---- Gravity toward center (0,0) ----
          if (config.gravity != 0.0f) {
              for (int i = 0; i < graph.node_count; ++i) {
                  if (graph.nodes[i].pinned) continue;
                  fx_buf[i] -= config.gravity * graph.nodes[i].x;
                  fy_buf[i] -= config.gravity * graph.nodes[i].y;
              }
          }

          // ---- Integrate: v = v * damping + F; pos += v ----
          total_energy = 0.0f;
          for (int i = 0; i < graph.node_count; ++i) {
              GraphNode& n = graph.nodes[i];
              if (n.pinned) continue;

              n.vx = n.vx * config.damping + fx_buf[i];
              n.vy = n.vy * config.damping + fy_buf[i];

              // Clamp velocity
              n.vx = std::max(-config.max_velocity, std::min(config.max_velocity, n.vx));
              n.vy = std::max(-config.max_velocity, std::min(config.max_velocity, n.vy));

              n.x += n.vx;
              n.y += n.vy;

              total_energy += n.vx * n.vx + n.vy * n.vy;
          }
      }

      graph.update_bounds();
      return total_energy;
  }

  void graph_force_layout_reset(GraphData& graph, float spread) {
      for (int i = 0; i < graph.node_count; ++i) {
          GraphNode& n = graph.nodes[i];
          if (n.pinned) continue;
          // rand() produces [0, RAND_MAX]; map to [-spread, spread]
          n.x = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
          n.y = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
          n.vx = 0.0f;
          n.vy = 0.0f;
      }
      graph.update_bounds();
  }

  void graph_layout_circular(GraphData& graph, float radius) {
      if (graph.node_count <= 0) return;
      const float two_pi = 6.28318530718f;
      for (int i = 0; i < graph.node_count; ++i) {
          GraphNode& n = graph.nodes[i];
          if (n.pinned) continue;
          float angle = two_pi * (float)i / (float)graph.node_count;
          n.x = radius * std::cos(angle);
          n.y = radius * std::sin(angle);
          n.vx = 0.0f;
          n.vy = 0.0f;
      }
      graph.update_bounds();
  }

  void graph_layout_grid(GraphData& graph, float spacing) {
      if (graph.node_count <= 0) return;
      int cols = (int)std::ceil(std::sqrt((float)graph.node_count));
      int rows = (graph.node_count + cols - 1) / cols;
      float ox = -0.5f * (cols - 1) * spacing;
      float oy = -0.5f * (rows - 1) * spacing;
      for (int i = 0; i < graph.node_count; ++i) {
          GraphNode& n = graph.nodes[i];
          if (n.pinned) continue;
          int col = i % cols;
          int row = i / cols;
          n.x = ox + col * spacing;
          n.y = oy + row * spacing;
          n.vx = 0.0f;
          n.vy = 0.0f;
      }
      graph.update_bounds();
  }

params_schema:
  {"params":[{"name":"graph","desc":"Referencia al grafo (GraphData) cuyos nodos se actualizan in-place. Modifica x, y, vx, vy de cada nodo no pinned."},{"name":"config","desc":"Parametros de la simulacion: repulsion (fuerza coulombiana), attraction (spring constant), damping (decay de velocidad), theta (precision Barnes-Hut 0=exacto/1=rapido), gravity (atraccion al centro), max_velocity, iterations."}],"output":"Energia cinetica total (suma de |v|^2). Cuando cae por debajo de un umbral elegido por el caller, el layout ha convergido y se puede dejar de llamar."}


id: graph_renderer_cpp_viz
name: graph_renderer
kind: function
lang: cpp
domain: viz
version: 1.0.0
purity: impure
signature: GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)
tags: ["graph","renderer","opengl","gpu","instanced","fbo","visualization"]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
error_type: error_go_core
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/graph_renderer.cpp
created_at: 2026-04-07T22:05:15Z
updated_at: 2026-04-07T22:05:15Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 454b3d858672beb05e1fe34e5c7691ea1cb09ea8f72fe370645e6d6a67960d27
description:
  Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes

example:
  ```cpp
  // Inicializacion (una vez)
  GraphRenderer* renderer = graph_renderer_create(800, 600);

  // En el render loop
  ImVec2 panel_size = ImGui::GetContentRegionAvail();
  graph_renderer_resize(renderer, (int)panel_size.x, (int)panel_size.y);

  unsigned int tex = graph_renderer_draw(renderer, graph_data,
                                          cam_x, cam_y, cam_zoom);

  ImGui::Image((ImTextureID)(uintptr_t)tex,
               panel_size,
               ImVec2(0, 1), ImVec2(1, 0)); // flip Y para OpenGL

  // Destruccion
  graph_renderer_destroy(renderer);
  ```

notes:
  **Renderizado de nodos:** Instanced rendering con un quad unitario [-0.5, 0.5] expandido por el tamano del nodo. El fragment shader aplica un SDF circular con anti-aliasing via `smoothstep` y un anillo de outline.

  **Renderizado de aristas:** `GL_LINES` con datos de posicion y color empaquetados por arista. El ancho se controla con `GraphRendererConfig::edge_width`.

  **Transformacion de camara:**
  ```
  tx = -cam_x * zoom + width/2
  ty = -cam_y * zoom + height/2
  ndc = (screen / viewport) * 2 - 1
  ```

  **Paleta de comunidades:** 10 colores ABGR usados cuando `node.color == 0`, seleccionados por `node.community % 10`.

  **Estado GL:** Guarda y restaura `GL_FRAMEBUFFER_BINDING` y `GL_VIEWPORT` para ser compatible con el render loop de ImGui sin efectos secundarios.

  **Includes GL:** Usa `#define GL_GLEXT_PROTOTYPES` + `<GL/gl.h>` + `<GL/glext.h>`. Si el proyecto carga funciones GL via glad/gl3w, reemplazar estos includes por el loader correspondiente.

documentation:
  # graph_renderer

  Renderer GPU de grafos basado en OpenGL 3.3 core profile con instanced rendering. Renderiza nodos y aristas de un `GraphData` a un FBO interno y retorna el texture ID para integracion directa con `ImGui::Image()`.

  ## Funciones del API

  ```cpp
  // Ciclo de vida
  GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config = {});
  void graph_renderer_destroy(GraphRenderer* r);
  void graph_renderer_resize(GraphRenderer* r, int width, int height);

  // Renderizado
  unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
                                    float cam_x, float cam_y, float cam_zoom);
  ```

code:
  #include "viz/graph_renderer.h"
  #include "viz/graph_types.h"

  #define GL_GLEXT_PROTOTYPES
  #include <GL/gl.h>
  #include <GL/glext.h>

  #include <cstdlib>
  #include <cstring>
  #include <cstdio>
  #include <cmath>

  // ---------------------------------------------------------------------------
  // Community palette (ABGR packed, 10 colors)
  // ---------------------------------------------------------------------------
  static const uint32_t k_palette[10] = {
      0xFF4CAF50, // green
      0xFFF44336, // red
      0xFF2196F3, // blue
      0xFFFF9800, // orange
      0xFF9C27B0, // purple
      0xFF00BCD4, // cyan
      0xFFFFEB3B, // yellow
      0xFFE91E63, // pink
      0xFF795548, // brown
      0xFF607D8B  // blue-grey
  };

  // ---------------------------------------------------------------------------
  // Internal struct
  // ---------------------------------------------------------------------------
  struct GraphRenderer {
      unsigned int fbo;
      unsigned int texture;
      unsigned int rbo; // depth/stencil renderbuffer
      int width, height;

      // Node rendering (instanced quads)
      unsigned int node_vao, node_quad_vbo, node_instance_vbo;
      unsigned int node_shader;

      // Edge rendering (lines)
      unsigned int edge_vao, edge_vbo;
      unsigned int edge_shader;

      GraphRendererConfig config;
  };

  // ---------------------------------------------------------------------------
  // Shader sources
  // ---------------------------------------------------------------------------

  // Node vertex shader — instanced unit quad
  static const char* k_node_vert = R"(
  #version 330 core
  // Quad corners [-0.5, 0.5]
  layout(location = 0) in vec2 a_quad;

  // Per-instance: world position, size, RGBA color
  layout(location = 1) in vec2  a_pos;
  layout(location = 2) in float a_size;
  layout(location = 3) in vec4  a_color;

  out vec2  v_uv;
  out vec4  v_color;

  uniform vec2  u_viewport;   // (width, height) in pixels
  uniform float u_scale;      // cam_zoom
  uniform vec2  u_translate;  // (tx, ty) in pixels

  void main() {
      // World -> screen (pixels)
      vec2 screen = a_pos * u_scale + u_translate;
      // Expand quad by node radius (size = diameter)
      screen += a_quad * a_size * u_scale;
      // Screen -> NDC
      vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
      ndc.y = -ndc.y; // flip Y (screen Y grows downward)
      gl_Position = vec4(ndc, 0.0, 1.0);
      v_uv    = a_quad + 0.5; // [0,1]
      v_color = a_color;
  }
  )";

  // Node fragment shader — SDF circle with outline
  static const char* k_node_frag = R"(
  #version 330 core
  in vec2 v_uv;
  in vec4 v_color;

  out vec4 frag_color;

  uniform float u_outline_px;  // outline width in uv units
  uniform float u_node_px;     // node diameter in pixels (= size * zoom)

  void main() {
      // SDF circle centered at (0.5, 0.5) in uv space
      float dist = length(v_uv - 0.5);
      float r    = 0.5;

      // Anti-alias edge (in uv units; 1px ~ 1/u_node_px in uv space)
      float fwidth_uv = 1.5 / max(u_node_px, 1.0);

      float alpha = 1.0 - smoothstep(r - fwidth_uv, r, dist);
      if (alpha < 0.001) discard;

      // Outline ring
      float outline_uv = u_outline_px / max(u_node_px, 1.0);
      float outline    = smoothstep(r - outline_uv - fwidth_uv, r - outline_uv, dist);

      vec3 fill    = v_color.rgb;
      vec3 outline_col = mix(fill, vec3(1.0), 0.6); // lighter outline
      vec3 color   = mix(fill, outline_col, outline);

      frag_color = vec4(color, v_color.a * alpha);
  }
  )";

  // Edge vertex shader
  static const char* k_edge_vert = R"(
  #version 330 core
  layout(location = 0) in vec2 a_pos;
  layout(location = 1) in vec4 a_color;

  out vec4 v_color;

  uniform vec2  u_viewport;
  uniform float u_scale;
  uniform vec2  u_translate;

  void main() {
      vec2 screen = a_pos * u_scale + u_translate;
      vec2 ndc    = (screen / u_viewport) * 2.0 - 1.0;
      ndc.y = -ndc.y;
      gl_Position = vec4(ndc, 0.0, 1.0);
      v_color = a_color;
  }
  )";

  // Edge fragment shader
  static const char* k_edge_frag = R"(
  #version 330 core
  in vec4 v_color;
  out vec4 frag_color;

  void main() {
      frag_color = v_color;
  }
  )";

  // ---------------------------------------------------------------------------
  // Shader helpers
  // ---------------------------------------------------------------------------
  static unsigned int compile_shader(GLenum type, const char* src) {
      unsigned int s = glCreateShader(type);
      glShaderSource(s, 1, &src, nullptr);
      glCompileShader(s);
      int ok;
      glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
      if (!ok) {
          char buf[512];
          glGetShaderInfoLog(s, sizeof(buf), nullptr, buf);
          fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf);
      }
      return s;
  }

  static unsigned int link_program(const char* vert_src, const char* frag_src) {
      unsigned int vs = compile_shader(GL_VERTEX_SHADER,   vert_src);
      unsigned int fs = compile_shader(GL_FRAGMENT_SHADER, frag_src);
      unsigned int prog = glCreateProgram();
      glAttachShader(prog, vs);
      glAttachShader(prog, fs);
      glLinkProgram(prog);
      int ok;
      glGetProgramiv(prog, GL_LINK_STATUS, &ok);
      if (!ok) {
          char buf[512];
          glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf);
          fprintf(stderr, "[graph_renderer] program link error: %s\n", buf);
      }
      glDeleteShader(vs);
      glDeleteShader(fs);
      return prog;
  }

  // ---------------------------------------------------------------------------
  // FBO helpers
  // ---------------------------------------------------------------------------
  static void create_fbo(GraphRenderer* r) {
      // Texture
      glGenTextures(1, &r->texture);
      glBindTexture(GL_TEXTURE_2D, r->texture);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, r->width, r->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      glBindTexture(GL_TEXTURE_2D, 0);

      // Depth renderbuffer
      glGenRenderbuffers(1, &r->rbo);
      glBindRenderbuffer(GL_RENDERBUFFER, r->rbo);
      glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, r->width, r->height);
      glBindRenderbuffer(GL_RENDERBUFFER, 0);

      // FBO
      glGenFramebuffers(1, &r->fbo);
      glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
      glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, r->texture, 0);
      glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, r->rbo);
      glBindFramebuffer(GL_FRAMEBUFFER, 0);
  }

  static void destroy_fbo(GraphRenderer* r) {
      glDeleteFramebuffers(1,  &r->fbo);
      glDeleteTextures(1,      &r->texture);
      glDeleteRenderbuffers(1, &r->rbo);
      r->fbo = r->texture = r->rbo = 0;
  }

  // ---------------------------------------------------------------------------
  // Helper: unpack ABGR uint32 to float RGBA
  // ---------------------------------------------------------------------------
  static inline void abgr_to_rgba(uint32_t abgr, float& r, float& g, float& b, float& a) {
      // ABGR layout: bits 31-24 = A, 23-16 = B, 15-8 = G, 7-0 = R
      a = ((abgr >> 24) & 0xFF) / 255.0f;
      b = ((abgr >> 16) & 0xFF) / 255.0f;
      g = ((abgr >>  8) & 0xFF) / 255.0f;
      r = ((abgr      ) & 0xFF) / 255.0f;
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config) {
      GraphRenderer* r = new GraphRenderer();
      r->width  = width;
      r->height = height;
      r->config = config;

      // --- FBO ---
      create_fbo(r);

      // --- Node VAO ---
      // Unit quad: 4 vertices, each (x, y) in [-0.5, 0.5]
      static const float quad_verts[8] = {
          -0.5f, -0.5f,
           0.5f, -0.5f,
          -0.5f,  0.5f,
           0.5f,  0.5f,
      };

      glGenVertexArrays(1, &r->node_vao);
      glBindVertexArray(r->node_vao);

      // Quad VBO (location 0)
      glGenBuffers(1, &r->node_quad_vbo);
      glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo);
      glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
      glEnableVertexAttribArray(0);
      glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);

      // Instance VBO (location 1,2,3 — position, size, color)
      glGenBuffers(1, &r->node_instance_vbo);
      glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
      // layout: x, y, size, r, g, b, a  — 7 floats per instance
      glEnableVertexAttribArray(1); // pos
      glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0);
      glVertexAttribDivisor(1, 1);
      glEnableVertexAttribArray(2); // size
      glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(2 * sizeof(float)));
      glVertexAttribDivisor(2, 1);
      glEnableVertexAttribArray(3); // color
      glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float)));
      glVertexAttribDivisor(3, 1);

      glBindVertexArray(0);

      // --- Edge VAO ---
      // Each edge: 2 vertices x (x, y, r, g, b, a) = 2 * 6 floats
      glGenVertexArrays(1, &r->edge_vao);
      glBindVertexArray(r->edge_vao);

      glGenBuffers(1, &r->edge_vbo);
      glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
      glEnableVertexAttribArray(0); // pos
      glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
      glEnableVertexAttribArray(1); // color
      glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(2 * sizeof(float)));

      glBindVertexArray(0);

      // --- Shaders ---
      r->node_shader = link_program(k_node_vert, k_node_frag);
      r->edge_shader = link_program(k_edge_vert, k_edge_frag);

      return r;
  }

  void graph_renderer_destroy(GraphRenderer* r) {
      if (!r) return;
      destroy_fbo(r);
      glDeleteVertexArrays(1, &r->node_vao);
      glDeleteBuffers(1, &r->node_quad_vbo);
      glDeleteBuffers(1, &r->node_instance_vbo);
      glDeleteVertexArrays(1, &r->edge_vao);
      glDeleteBuffers(1, &r->edge_vbo);
      glDeleteProgram(r->node_shader);
      glDeleteProgram(r->edge_shader);
      delete r;
  }

  void graph_renderer_resize(GraphRenderer* r, int width, int height) {
      if (!r) return;
      if (r->width == width && r->height == height) return;
      r->width  = width;
      r->height = height;
      destroy_fbo(r);
      create_fbo(r);
  }

  unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
                                    float cam_x, float cam_y, float cam_zoom) {
      if (!r) return 0;

      // --- Save GL state ---
      GLint prev_fbo;
      glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
      GLint prev_viewport[4];
      glGetIntegerv(GL_VIEWPORT, prev_viewport);

      // --- Bind FBO ---
      glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
      glViewport(0, 0, r->width, r->height);

      // Clear with bg_color (ABGR)
      float bg_a, bg_b, bg_g, bg_cr;
      abgr_to_rgba(r->config.bg_color, bg_cr, bg_g, bg_b, bg_a);
      glClearColor(bg_cr, bg_g, bg_b, bg_a);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

      // Enable blending for anti-aliasing and transparency
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

      // View transform: world -> screen pixels
      // tx = -cam_x * scale + width/2
      // ty = -cam_y * scale + height/2
      float scale = cam_zoom;
      float tx = -cam_x * scale + (float)r->width  * 0.5f;
      float ty = -cam_y * scale + (float)r->height * 0.5f;

      // ----------------------------------------------------------------
      // Draw edges
      // ----------------------------------------------------------------
      if (graph.edge_count > 0 && graph.edges && graph.nodes) {
          // Pack: 2 vertices per edge, each vertex = (x, y, r, g, b, a) = 6 floats
          const int floats_per_edge = 2 * 6;
          float* edge_buf = (float*)malloc((size_t)graph.edge_count * floats_per_edge * sizeof(float));
          int vi = 0;
          for (int i = 0; i < graph.edge_count; ++i) {
              const GraphEdge& e = graph.edges[i];
              uint32_t ecol = e.color != 0 ? e.color : 0xFF888888u; // default gray
              float er, eg, eb, ea;
              abgr_to_rgba(ecol, er, eg, eb, ea);
              ea *= r->config.edge_alpha;

              if (e.source < (uint32_t)graph.node_count && e.target < (uint32_t)graph.node_count) {
                  const GraphNode& ns = graph.nodes[e.source];
                  const GraphNode& nt = graph.nodes[e.target];

                  // Source vertex
                  edge_buf[vi++] = ns.x; edge_buf[vi++] = ns.y;
                  edge_buf[vi++] = er;   edge_buf[vi++] = eg;
                  edge_buf[vi++] = eb;   edge_buf[vi++] = ea;
                  // Target vertex
                  edge_buf[vi++] = nt.x; edge_buf[vi++] = nt.y;
                  edge_buf[vi++] = er;   edge_buf[vi++] = eg;
                  edge_buf[vi++] = eb;   edge_buf[vi++] = ea;
              }
          }

          glUseProgram(r->edge_shader);
          glUniform2f(glGetUniformLocation(r->edge_shader, "u_viewport"), (float)r->width, (float)r->height);
          glUniform1f(glGetUniformLocation(r->edge_shader, "u_scale"),    scale);
          glUniform2f(glGetUniformLocation(r->edge_shader, "u_translate"), tx, ty);

          glLineWidth(r->config.edge_width);

          glBindVertexArray(r->edge_vao);
          glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
          glBufferData(GL_ARRAY_BUFFER, vi * (int)sizeof(float), edge_buf, GL_DYNAMIC_DRAW);
          glDrawArrays(GL_LINES, 0, vi / 6);
          glBindVertexArray(0);

          free(edge_buf);
      }

      // ----------------------------------------------------------------
      // Draw nodes (instanced quads)
      // ----------------------------------------------------------------
      if (graph.node_count > 0 && graph.nodes) {
          // Pack: 7 floats per node: x, y, size, r, g, b, a
          float* node_buf = (float*)malloc((size_t)graph.node_count * 7 * sizeof(float));
          for (int i = 0; i < graph.node_count; ++i) {
              const GraphNode& n = graph.nodes[i];
              uint32_t ncol = n.color != 0 ? n.color : k_palette[n.community % 10];
              float nr, ng, nb, na;
              abgr_to_rgba(ncol, nr, ng, nb, na);

              float sz = n.size > 0.0f ? n.size : 4.0f;
              float* p = node_buf + i * 7;
              p[0] = n.x; p[1] = n.y; p[2] = sz;
              p[3] = nr;  p[4] = ng;  p[5] = nb; p[6] = na;
          }

          glUseProgram(r->node_shader);
          glUniform2f(glGetUniformLocation(r->node_shader, "u_viewport"),  (float)r->width, (float)r->height);
          glUniform1f(glGetUniformLocation(r->node_shader, "u_scale"),     scale);
          glUniform2f(glGetUniformLocation(r->node_shader, "u_translate"), tx, ty);
          glUniform1f(glGetUniformLocation(r->node_shader, "u_outline_px"), r->config.node_outline);

          glBindVertexArray(r->node_vao);
          glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
          glBufferData(GL_ARRAY_BUFFER, graph.node_count * 7 * (int)sizeof(float), node_buf, GL_DYNAMIC_DRAW);

          // Draw 4 vertices (triangle strip quad) x node_count instances
          // Pass per-instance node_px uniform via the average size (approximation)
          // For exact per-node pixel size we'd need a texture or another approach;
          // use a uniform average for AA quality — good enough for most graphs.
          float avg_px = 8.0f * scale; // rough estimate
          glUniform1f(glGetUniformLocation(r->node_shader, "u_node_px"), avg_px);

          glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, graph.node_count);
          glBindVertexArray(0);

          free(node_buf);
      }

      // --- Restore GL state ---
      glDisable(GL_BLEND);
      glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
      glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);

      return r->texture;
  }

params_schema:
  {"params":[{"name":"width","desc":"Ancho del framebuffer en pixels"},{"name":"height","desc":"Alto del framebuffer en pixels"},{"name":"config","desc":"Configuracion visual: outline width, edge width, edge alpha, color de fondo, fade de aristas por distancia a camara"}],"output":"Handle opaco al renderer. Usar graph_renderer_draw() para obtener texture ID de OpenGL, pasable directamente a ImGui::Image()"}


id: graph_viewport_cpp_viz
name: graph_viewport
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: impure
signature: bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size)
tags: ["graph","viewport","imgui","interactive","pan","zoom","dashboard"]
uses_functions: ["graph_renderer_cpp_viz","graph_force_layout_cpp_viz","graph_spatial_hash_cpp_core"]
uses_types: ["GraphData_cpp_viz"]
returns: []
error_type: error_go_core
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/graph_viewport.cpp
created_at: 2026-04-07T22:05:15Z
updated_at: 2026-04-07T22:05:15Z
emits: []
framework: imgui
variant: []
content_hash: 724d590828b6a821060c7dee9eb04d38f36dcf9811fd6674045c9dfd83312e85
description:
  Widget ImGui completo para visualizacion interactiva de grafos con pan, zoom, hover, seleccion y layout en vivo

props:
  [{"name":"id","type":"const char*","required":true,"description":"Identificador unico del widget ImGui"},{"name":"graph","type":"GraphData\u0026","required":true,"description":"Referencia al grafo (lectura de datos, escritura de posiciones al drag)"},{"name":"state","type":"GraphViewportState\u0026","required":true,"description":"Estado persistente del viewport (camera, seleccion, renderer). Debe vivir mas que los frames."},{"name":"size","type":"ImVec2","required":false,"description":"Tamanio del widget en pixeles. ImVec2(0,0) usa todo el espacio disponible."}]

notes:
  - Usa `ImGui::InvisibleButton` con flags para los tres botones del raton, capturando input sin dibujar ningun boton visible.
  - La textura del renderer se muestra con UV volteado en Y (`ImVec2(0,1)` a `ImVec2(1,0)`) para corregir la convencion de coordenadas de OpenGL vs ImGui.
  - El spatial hash se reconstruye cada frame desde las posiciones actuales de los nodos, garantizando hit-testing correcto despues de drag o layout.
  - El zoom hacia el cursor mantiene el punto del grafo bajo el cursor fijo en pantalla ajustando `cam_x`/`cam_y`.

documentation:
  # graph_viewport

  Widget ImGui self-contained para visualizar grafos interactivos. Integra rendering GPU, force-directed layout y hit-testing espacial en una sola llamada por frame.

  ## Uso basico

  ```cpp
  // Declarar estado persistente (fuera del loop de render)
  GraphViewportState vp_state;

  // En el loop de render (dentro de una ventana ImGui):
  if (graph_viewport("mi_grafo", my_graph, vp_state)) {
      // hubo interaccion este frame
      if (vp_state.selected_node >= 0) {
          auto& n = my_graph.nodes[vp_state.selected_node];
          // mostrar panel de detalle de n
      }
  }

  // Al terminar:
  graph_viewport_destroy(vp_state);
  ```

  ## Estado de camara

  La camara usa coordenadas del espacio del grafo:

  - `cam_x`, `cam_y`: centro de la camara en espacio del grafo
  - `zoom`: pixeles por unidad de grafo

  `graph_viewport_fit()` centra y ajusta el zoom para que el grafo quepa con 10% de padding.

  ## Controles

  | Accion | Control |
  |--------|---------|
  | Pan | Boton medio o derecho + arrastrar |
  | Zoom | Rueda del raton (hacia el cursor) |
  | Seleccionar nodo | Click izquierdo |
  | Arrastrar nodo | Click izquierdo sobre nodo |
  | Toggle layout | Barra espaciadora |
  | Fit camara | F |

  ## Force layout

  El layout se ejecuta automaticamente cada frame mientras `state.layout_running == true`. Se detiene solo cuando la energia cinetica cae por debajo de `0.01`. Se puede pausar/reanudar con la barra espaciadora.

  Los nodos arrastrados se marcan como `pinned = true` durante el drag, impidiendo que el force layout los mueva. Al soltar, `pinned` vuelve a `false`.

  ## Tooltip

  Al hacer hover sobre un nodo se muestra un tooltip con: label, id numerico, community, degree (aristas conectadas) y value.

  ## Status bar

  En la parte inferior del widget aparece: numero de nodos, aristas, zoom actual, energia del layout y recordatorio de atajos de teclado.

  ## Inicializacion lazy

  El renderer OpenGL y el spatial hash se crean en el primer frame. La camara se ajusta automaticamente con `graph_viewport_fit` en la inicializacion.

code:
  #include "viz/graph_viewport.h"
  #include "viz/graph_types.h"
  #include "viz/graph_renderer.h"
  #include "viz/graph_force_layout.h"
  #include "core/graph_spatial_hash.h"
  #include "imgui.h"

  #include <cstdio>   // snprintf
  #include <cstring>  // memset
  #include <vector>

  // ---------------------------------------------------------------------------
  // Internal helpers
  // ---------------------------------------------------------------------------

  static void viewport_to_graph(float vx, float vy,
                                 float widget_x, float widget_y,
                                 float widget_w, float widget_h,
                                 float cam_x, float cam_y, float zoom,
                                 float& gx, float& gy)
  {
      gx = (vx - widget_x - widget_w * 0.5f) / zoom + cam_x;
      gy = (vy - widget_y - widget_h * 0.5f) / zoom + cam_y;
  }

  // ---------------------------------------------------------------------------
  // graph_viewport_fit
  // ---------------------------------------------------------------------------

  void graph_viewport_fit(GraphData& graph, GraphViewportState& state)
  {
      graph.update_bounds();
      if (graph.node_count == 0) {
          state.cam_x = 0.0f;
          state.cam_y = 0.0f;
          state.zoom  = 1.0f;
          return;
      }

      float cx = (graph.min_x + graph.max_x) * 0.5f;
      float cy = (graph.min_y + graph.max_y) * 0.5f;
      state.cam_x = cx;
      state.cam_y = cy;

      float span_x = graph.max_x - graph.min_x;
      float span_y = graph.max_y - graph.min_y;
      float span   = (span_x > span_y ? span_x : span_y);

      // Use render dimensions if available; fall back to a safe default.
      float view_px = (state.render_w > 0 ? (float)state.render_w : 600.0f);
      float view_py = (state.render_h > 0 ? (float)state.render_h : 400.0f);
      float view_min = (view_px < view_py ? view_px : view_py);

      if (span > 0.0f) {
          state.zoom = (view_min * 0.9f) / span;
      } else {
          state.zoom = 1.0f;
      }

      // Clamp to allowed range
      if (state.zoom < state.zoom_min) state.zoom = state.zoom_min;
      if (state.zoom > state.zoom_max) state.zoom = state.zoom_max;
  }

  // ---------------------------------------------------------------------------
  // graph_viewport_destroy
  // ---------------------------------------------------------------------------

  void graph_viewport_destroy(GraphViewportState& state)
  {
      if (state.renderer) {
          graph_renderer_destroy(state.renderer);
          state.renderer = nullptr;
      }
      if (state.spatial) {
          delete state.spatial;
          state.spatial = nullptr;
      }
      state.initialized = false;
  }

  // ---------------------------------------------------------------------------
  // graph_viewport — main widget
  // ---------------------------------------------------------------------------

  bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
                      ImVec2 size)
  {
      bool interacted = false;

      // Resolve size
      ImVec2 avail = ImGui::GetContentRegionAvail();
      float w = (size.x > 0.0f) ? size.x : avail.x;
      float h = (size.y > 0.0f) ? size.y : avail.y;
      if (w < 1.0f) w = 1.0f;
      if (h < 1.0f) h = 1.0f;

      int iw = (int)w, ih = (int)h;

      // -------------------------------------------------------------------
      // 1. Lazy init
      // -------------------------------------------------------------------
      if (!state.initialized) {
          state.renderer  = graph_renderer_create(iw, ih);
          state.spatial   = new SpatialHash(20.0f, 4096);
          state.render_w  = iw;
          state.render_h  = ih;
          state.initialized = true;
          graph_viewport_fit(graph, state);
      }

      // -------------------------------------------------------------------
      // 2. Resize
      // -------------------------------------------------------------------
      if (iw != state.render_w || ih != state.render_h) {
          graph_renderer_resize(state.renderer, iw, ih);
          state.render_w = iw;
          state.render_h = ih;
      }

      // -------------------------------------------------------------------
      // 3. Force layout step
      // -------------------------------------------------------------------
      if (state.layout_running && graph.node_count > 0) {
          state.layout_energy = graph_force_layout_step(graph);
          if (state.layout_energy < 0.01f) {
              state.layout_running = false;
          }
      }

      // -------------------------------------------------------------------
      // 4. Build spatial hash
      // -------------------------------------------------------------------
      if (graph.node_count > 0) {
          static std::vector<float> xs_buf, ys_buf, sz_buf;
          xs_buf.resize(graph.node_count);
          ys_buf.resize(graph.node_count);
          sz_buf.resize(graph.node_count);
          for (int i = 0; i < graph.node_count; ++i) {
              xs_buf[i] = graph.nodes[i].x;
              ys_buf[i] = graph.nodes[i].y;
              sz_buf[i] = graph.nodes[i].size;
          }
          state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count);
      }

      // -------------------------------------------------------------------
      // 5. Invisible button to capture input
      // -------------------------------------------------------------------
      ImGui::PushID(id);

      ImVec2 widget_pos = ImGui::GetCursorScreenPos();
      ImGui::InvisibleButton("canvas", ImVec2(w, h),
                             ImGuiButtonFlags_MouseButtonLeft  |
                             ImGuiButtonFlags_MouseButtonMiddle|
                             ImGuiButtonFlags_MouseButtonRight);

      bool hovered  = ImGui::IsItemHovered();
      bool lm_down  = ImGui::IsMouseDown(ImGuiMouseButton_Left);
      bool lm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
      bool mm_down  = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
      bool rm_down  = ImGui::IsMouseDown(ImGuiMouseButton_Right);

      ImVec2 mouse_pos = ImGui::GetMousePos();
      float  mx = mouse_pos.x, my = mouse_pos.y;

      // Convert mouse to graph space
      float gx_mouse, gy_mouse;
      viewport_to_graph(mx, my,
                        widget_pos.x, widget_pos.y, w, h,
                        state.cam_x, state.cam_y, state.zoom,
                        gx_mouse, gy_mouse);

      // -------------------------------------------------------------------
      // 5a. Pan (middle or right mouse drag)
      // -------------------------------------------------------------------
      if (hovered && (mm_down || rm_down)) {
          ImVec2 delta = ImGui::GetIO().MouseDelta;
          if (delta.x != 0.0f || delta.y != 0.0f) {
              state.cam_x -= delta.x / state.zoom;
              state.cam_y -= delta.y / state.zoom;
              interacted = true;
          }
      }

      // -------------------------------------------------------------------
      // 5b. Zoom (scroll wheel)
      // -------------------------------------------------------------------
      if (hovered) {
          float wheel = ImGui::GetIO().MouseWheel;
          if (wheel != 0.0f) {
              float old_zoom = state.zoom;
              float new_zoom = old_zoom * (1.0f + wheel * 0.1f);
              if (new_zoom < state.zoom_min) new_zoom = state.zoom_min;
              if (new_zoom > state.zoom_max) new_zoom = state.zoom_max;

              // Zoom toward cursor: keep gx_mouse/gy_mouse fixed in graph space
              float rel_x = (mx - widget_pos.x - w * 0.5f);
              float rel_y = (my - widget_pos.y - h * 0.5f);
              state.cam_x += rel_x / old_zoom - rel_x / new_zoom;
              state.cam_y += rel_y / old_zoom - rel_y / new_zoom;
              state.zoom   = new_zoom;
              interacted   = true;
          }
      }

      // -------------------------------------------------------------------
      // 5c. Hover — query nearest node
      // -------------------------------------------------------------------
      state.hovered_node = -1;
      if (hovered && graph.node_count > 0) {
          float hit_radius = 10.0f / state.zoom;
          int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius);
          if (nearest >= 0) {
              state.hovered_node = nearest;
              interacted = true;
          }
      }

      // -------------------------------------------------------------------
      // 5d. Node drag (left mouse down on a node)
      // -------------------------------------------------------------------
      if (hovered && lm_down) {
          if (state.drag_node == -1 && state.hovered_node >= 0) {
              state.drag_node   = state.hovered_node;
              state.is_dragging = true;
          }
      } else {
          // Release drag
          if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
              graph.nodes[state.drag_node].pinned = false;
          }
          state.drag_node   = -1;
          state.is_dragging = false;
      }

      if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
          GraphNode& n = graph.nodes[state.drag_node];
          n.x      = gx_mouse;
          n.y      = gy_mouse;
          n.vx     = 0.0f;
          n.vy     = 0.0f;
          n.pinned = true;
          interacted = true;
      }

      // -------------------------------------------------------------------
      // 5e. Click — select node
      // -------------------------------------------------------------------
      if (hovered && lm_click && state.drag_node == -1) {
          state.selected_node = state.hovered_node;
          interacted = true;
      }

      // -------------------------------------------------------------------
      // 5f. Keyboard shortcuts (only when widget is active/hovered)
      // -------------------------------------------------------------------
      if (hovered) {
          if (ImGui::IsKeyPressed(ImGuiKey_Space)) {
              state.layout_running = !state.layout_running;
          }
          if (ImGui::IsKeyPressed(ImGuiKey_F)) {
              graph_viewport_fit(graph, state);
              interacted = true;
          }
      }

      // -------------------------------------------------------------------
      // 6. Render to GPU texture
      // -------------------------------------------------------------------
      unsigned int tex_id = graph_renderer_draw(state.renderer, graph,
                                                state.cam_x, state.cam_y,
                                                state.zoom);

      // -------------------------------------------------------------------
      // 7. Display texture (flip UV for OpenGL FBO convention)
      // -------------------------------------------------------------------
      ImDrawList* draw_list = ImGui::GetWindowDrawList();
      draw_list->AddImage(
          (ImTextureID)(intptr_t)tex_id,
          widget_pos,
          ImVec2(widget_pos.x + w, widget_pos.y + h),
          ImVec2(0.0f, 1.0f),   // UV top-left   (flipped Y)
          ImVec2(1.0f, 0.0f)    // UV bottom-right
      );

      // -------------------------------------------------------------------
      // 8. Tooltip on hovered node
      // -------------------------------------------------------------------
      if (state.hovered_node >= 0 && state.hovered_node < graph.node_count) {
          const GraphNode& n = graph.nodes[state.hovered_node];

          // Count degree
          int degree = 0;
          for (int i = 0; i < graph.edge_count; ++i) {
              if ((int)graph.edges[i].source == state.hovered_node ||
                  (int)graph.edges[i].target == state.hovered_node) {
                  ++degree;
              }
          }

          ImGui::BeginTooltip();
          if (n.label) ImGui::TextUnformatted(n.label);
          ImGui::Text("ID: %u", n.id);
          ImGui::Text("Community: %u", n.community);
          ImGui::Text("Degree: %d", degree);
          ImGui::Text("Value: %.3f", n.value);
          ImGui::EndTooltip();
      }

      // -------------------------------------------------------------------
      // 9. Status bar overlay
      // -------------------------------------------------------------------
      {
          char status[128];
          snprintf(status, sizeof(status),
                   "Nodes: %d | Edges: %d | Zoom: %.2fx | Energy: %.4f | [Space] layout [F] fit",
                   graph.node_count, graph.edge_count,
                   state.zoom, state.layout_energy);

          ImVec2 text_pos = ImVec2(widget_pos.x + 6.0f, widget_pos.y + h - 18.0f);
          draw_list->AddText(text_pos, IM_COL32(180, 180, 180, 200), status);
      }

      ImGui::PopID();
      return interacted;
  }

params_schema:
  {"params":[{"name":"id","desc":"Identificador unico del widget ImGui. Debe ser estable entre frames."},{"name":"graph","desc":"Grafo a visualizar. Las posiciones de nodos se modifican al arrastrar."},{"name":"state","desc":"Estado persistente: camara (cam_x, cam_y, zoom), nodo seleccionado/hovereado, renderer GPU, spatial hash. Alojado por el caller."},{"name":"size","desc":"Tamanio del widget en pixeles. (0,0) ocupa todo el espacio disponible en la ventana ImGui."}],"output":"true si hubo alguna interaccion del usuario en el frame actual (hover, click, drag, zoom, teclado)"}


id: heatmap_cpp_viz
name: heatmap
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: Renderiza un mapa de calor 2D usando ImPlot dentro de un frame ImGui
tags: ["implot","chart","visualization","gpu","heatmap","matrix"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot"]
tests: []
file_path: cpp/functions/viz/heatmap.cpp
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
framework: imgui
variant: []
content_hash: c89f29bd604bbed08bd575ac034f814bd8aab8e96108670f1f1a735512079945
signature:
  void heatmap(const char* title, const float* values, int rows, int cols, float scale_min, float scale_max)

documentation:
  # heatmap

  Wrapper atomico sobre `ImPlot::PlotHeatmap`. Renderiza una matriz de valores como mapa de calor con escala de color.

  Los datos deben estar en formato row-major. Si `scale_min` y `scale_max` son ambos 0, ImPlot autodetecta el rango.

code:
  #include "viz/heatmap.h"
  #include "implot.h"

  void heatmap(const char* title, const float* values, int rows, int cols,
               float scale_min, float scale_max) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_NoLegend)) {
          ImPlot::SetupAxes(nullptr, nullptr,
              ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
          ImPlot::PlotHeatmap("##data", values, rows, cols,
                              scale_min, scale_max);
          ImPlot::EndPlot();
      }
  }

  void heatmap(const char* title, const double* values, int rows, int cols,
               double scale_min, double scale_max) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_NoLegend)) {
          ImPlot::SetupAxes(nullptr, nullptr,
              ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
          ImPlot::PlotHeatmap("##data", values, rows, cols,
                              scale_min, scale_max);
          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del heatmap"},{"name":"values","desc":"Array de valores en orden row-major (values[row * cols + col])"},{"name":"rows","desc":"Numero de filas de la matriz"},{"name":"cols","desc":"Numero de columnas de la matriz"},{"name":"scale_min","desc":"Valor minimo de la escala de color (0 para autodetectar)"},{"name":"scale_max","desc":"Valor maximo de la escala de color (0 para autodetectar)"}],"output":"Renderiza el heatmap en el frame ImGui actual"}


id: histogram_cpp_viz
name: histogram
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
signature: void histogram(const char* title, const float* values, int count, int bins = -1)
tags: ["implot","chart","visualization","gpu","histogram","distribution"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot"]
tests: []
file_path: cpp/functions/viz/histogram.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 48df96a635bfb690826e054e985cd330ea33d6498a24ab8d3bd45b5a67024982
description:
  Renderiza un histograma con bins automaticos o manuales usando ImPlot PlotHistogram dentro de un frame ImGui

documentation:
  # histogram

  Wrapper atomico sobre `ImPlot::PlotHistogram` con seleccion automatica del numero de bins.

  Cuando `bins == -1` usa `ImPlotBin_Sturges`, que calcula el numero de bins como `ceil(log2(n)) + 1`. Para distribuciones con muchos valores o alta varianza puede preferirse pasar un valor explicito.

  El plot usa `ImVec2(-1, 0)` para ocupar el ancho disponible con altura automatica.

  Debe llamarse dentro del render callback de `fn::run_app`.

code:
  #include "viz/histogram.h"
  #include "implot.h"

  void histogram(const char* title, const float* values, int count, int bins) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          int b = (bins > 0) ? bins : ImPlotBin_Sturges;
          ImPlot::PlotHistogram("##data", values, count, b);
          ImPlot::EndPlot();
      }
  }

  void histogram(const char* title, const double* values, int count, int bins) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          int b = (bins > 0) ? bins : ImPlotBin_Sturges;
          ImPlot::PlotHistogram("##data", values, count, b);
          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del histograma mostrado como cabecera del plot"},{"name":"values","desc":"Array de valores numericos a distribuir en bins"},{"name":"count","desc":"Numero de valores en el array"},{"name":"bins","desc":"Numero de bins. -1 = automatico via regla de Sturges (ImPlotBin_Sturges). Positivo = numero explicito de bins"}],"output":"Renderiza el histograma en el frame ImGui actual"}


id: kpi_card_cpp_viz
name: kpi_card
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: Card de KPI con valor grande, delta porcentual, y sparkline historico para dashboards
tags: ["imgui","kpi","card","dashboard","metrics","sparkline"]
uses_functions: ["sparkline_cpp_viz"]
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/kpi_card.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: e226b3d68227148a0fbac12c2e36243fda7743a99a57a0b0d66da645e6523996
signature:
  void kpi_card(const char* label, float value, float delta_percent, const float* history = nullptr, int history_count = 0, const char* format = "%.1f")

example:
  ```cpp
  float history[] = {10.0f, 12.0f, 11.0f, 15.0f, 18.0f, 17.0f, 20.0f};
  kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f");

  // Sin sparkline
  kpi_card("Error Rate", 0.3f, -15.2f, nullptr, 0, "%.2f%%");

  // Grid de KPIs
  ImGui::Columns(3, "kpis", false);
  kpi_card("MAU",     1250000.0f,  3.4f, mau_history, 30);
  ImGui::NextColumn();
  kpi_card("Revenue", 89400.0f,   -1.2f, rev_history, 30, "$%.0f");
  ImGui::NextColumn();
  kpi_card("Churn",   2.1f,       -0.3f, churn_history, 30, "%.1f%%");
  ImGui::Columns(1);
  ```

notes:
  - El ancho total del grupo es aproximadamente 150px, apto para grids de 2-4 columnas.
  - El escalado de fuente usa `SetWindowFontScale(1.8f)` — compatible con cualquier fuente cargada; no requiere fonts adicionales.
  - Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) requieren que la fuente ImGui tenga el rango de simbolos geometricos cargado, o bien sustituir por ASCII (`^` y `v`).
  - El color verde del delta es `ImVec4(0.20, 0.80, 0.35, 1.0)` y el rojo `ImVec4(0.90, 0.25, 0.25, 1.0)`, coherentes con los colores del sparkline subyacente.
  - `BeginGroup`/`EndGroup` permite usar `SameLine()` despues de `kpi_card` y que el cursor avance correctamente.

documentation:
  # kpi_card

  Card compacta para dashboards ImGui que muestra un KPI con contexto de tendencia. Combina label, valor escalado, badge de delta colorizado y sparkline historico en un grupo coherente de ~150px de ancho.

  Usa `sparkline` del registry para el historico, con el mismo color que el badge (verde si delta >= 0, rojo si delta < 0).

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).

code:
  #include "kpi_card.h"
  #include "sparkline.h"
  #include <imgui.h>
  #include <cstdio>

  void kpi_card(const char* label, float value, float delta_percent,
                const float* history, int history_count,
                const char* format) {
      ImGui::BeginGroup();

      // Label — small, muted
      ImGui::TextDisabled("%s", label);

      // Value — scaled up font
      ImGui::SetWindowFontScale(1.8f);
      char value_buf[64];
      snprintf(value_buf, sizeof(value_buf), format, value);
      ImGui::Text("%s", value_buf);
      ImGui::SetWindowFontScale(1.0f);

      // Delta badge — green up arrow / red down arrow
      const bool positive = delta_percent >= 0.0f;
      const ImVec4 delta_color = positive
          ? ImVec4(0.20f, 0.80f, 0.35f, 1.0f)   // green
          : ImVec4(0.90f, 0.25f, 0.25f, 1.0f);  // red

      char delta_buf[32];
      if (positive) {
          snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xb2 +%.1f%%", delta_percent);
      } else {
          snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xbc %.1f%%", delta_percent);
      }

      ImGui::PushStyleColor(ImGuiCol_Text, delta_color);
      ImGui::Text("%s", delta_buf);
      ImGui::PopStyleColor();

      // Sparkline — matches delta color
      if (history != nullptr && history_count > 0) {
          sparkline(label, history, history_count, delta_color, 120.0f, 24.0f);
      }

      ImGui::EndGroup();
  }

params_schema:
  {"params":[{"name":"label","desc":"Nombre del KPI mostrado como header muted (ej: \"Revenue\", \"Users\")"},{"name":"value","desc":"Valor numerico actual del KPI"},{"name":"delta_percent","desc":"Cambio porcentual respecto al periodo anterior (positivo = mejora, negativo = deterioro)"},{"name":"history","desc":"Array de valores historicos para el sparkline. Nullable — si es nullptr no se renderiza sparkline"},{"name":"history_count","desc":"Numero de valores en el array history"},{"name":"format","desc":"Formato printf para el valor principal (ej: \"$%.0f\", \"%.1f%%\", \"%.2f\")"}],"output":"Renderiza la card KPI completa en el frame ImGui actual: label muted, valor grande, badge delta verde/rojo con triangulo, y sparkline de 120x24px"}


id: line_plot_cpp_viz
name: line_plot
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
signature: void line_plot(const char* title, const float* xs, const float* ys, int count)
description: Renderiza un grafico de lineas 2D usando ImPlot dentro de un frame ImGui
tags: ["implot","chart","visualization","gpu","line"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot"]
tests: []
file_path: cpp/functions/viz/line_plot.cpp
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 0d1dcfcea545da3a000a003dbd7cfdde13494e8ecad1cc8dd16f02cc938133cb
documentation:
  # line_plot

  Wrapper atomico sobre `ImPlot::PlotLine`. Renderiza un grafico de lineas 2D con los datos proporcionados.

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).

  Soporta `float` y `double` precision.

code:
  #include "viz/line_plot.h"
  #include "implot.h"

  void line_plot(const char* title, const float* xs, const float* ys, int count) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          ImPlot::PlotLine("##data", xs, ys, count);
          ImPlot::EndPlot();
      }
  }

  void line_plot(const char* title, const double* xs, const double* ys, int count) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          ImPlot::PlotLine("##data", xs, ys, count);
          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del grafico, se muestra como header del plot"},{"name":"xs","desc":"Array de coordenadas X"},{"name":"ys","desc":"Array de coordenadas Y"},{"name":"count","desc":"Numero de puntos en los arrays xs/ys"}],"output":"Renderiza el grafico de lineas en el frame ImGui actual"}


id: pie_chart_cpp_viz
name: pie_chart
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: Renderiza un grafico circular (pie/donut) usando ImPlot PlotPieChart dentro de un frame ImGui
tags: ["implot","chart","visualization","gpu","pie","donut"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot"]
tests: []
file_path: cpp/functions/viz/pie_chart.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T22:21:10Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 144b3ead5db0e04a676cd844619816224cffb78053a175e2d2fa37e19f2f55ce
signature:
  void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f)

documentation:
  # pie_chart

  Wrapper atomico sobre `ImPlot::PlotPieChart` con soporte para modo pie y modo donut.

  El eje del plot se configura con `ImPlotAxisFlags_NoDecorations` para ocultar los ejes y mostrar solo el grafico circular. El aspecto se fuerza a cuadrado con `ImPlotFlags_Equal`.

  **Modo pie** (`radius >= 0`): dibuja un pie chart solido. Si `radius == 0`, usa radio automatico de 0.4.

  **Modo donut** (`radius < 0`): usa `|radius|` como radio exterior. El agujero interior es fijo en 0.2, suficiente para texto central.

  Debe llamarse dentro del render callback de `fn::run_app`.

code:
  #include "viz/pie_chart.h"
  #include "implot.h"

  void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) {
          ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
          ImPlot::SetupAxesLimits(0, 1, 0, 1);
          if (radius < 0.0f) {
              ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, static_cast<double>(-radius), "%.1f", 90.0);
          } else {
              float r = (radius > 0.0f) ? radius : 0.4f;
              ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, static_cast<double>(r), "%.1f", 90.0);
          }
          ImPlot::EndPlot();
      }
  }

  void pie_chart(const char* title, const char* const* labels, const double* values, int count, double radius) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) {
          ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
          ImPlot::SetupAxesLimits(0, 1, 0, 1);
          if (radius < 0.0) {
              ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, -radius, "%.1f", 90.0);
          } else {
              double r = (radius > 0.0) ? radius : 0.4;
              ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0);
          }
          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del grafico"},{"name":"labels","desc":"Array de etiquetas para cada segmento del pie"},{"name":"values","desc":"Array de valores numericos para cada segmento"},{"name":"count","desc":"Numero de segmentos (longitud de labels y values)"},{"name":"radius","desc":"Radio del pie (0 = auto 0.4). Positivo = radio explicito. Negativo = modo donut con outer radius = |radius| e inner = 0.2"}],"output":"Renderiza el grafico circular en el frame ImGui actual"}


id: scatter_plot_cpp_viz
name: scatter_plot
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
signature: void scatter_plot(const char* title, const float* xs, const float* ys, int count)
description: Renderiza un grafico de dispersion usando ImPlot dentro de un frame ImGui
tags: ["implot","chart","visualization","gpu","scatter"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot"]
tests: []
file_path: cpp/functions/viz/scatter_plot.cpp
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 0c6a73764dfdadd8538941a01048d05994e85d8484f488fff8c6d42c477a0842
documentation:
  # scatter_plot

  Wrapper atomico sobre `ImPlot::PlotScatter`. Renderiza un grafico de dispersion 2D.

  Debe llamarse dentro del render callback de `fn::run_app`.

code:
  #include "viz/scatter_plot.h"
  #include "implot.h"

  void scatter_plot(const char* title, const float* xs, const float* ys, int count) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          ImPlot::PlotScatter("##data", xs, ys, count);
          ImPlot::EndPlot();
      }
  }

  void scatter_plot(const char* title, const double* xs, const double* ys, int count) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          ImPlot::PlotScatter("##data", xs, ys, count);
          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del grafico scatter"},{"name":"xs","desc":"Array de coordenadas X"},{"name":"ys","desc":"Array de coordenadas Y"},{"name":"count","desc":"Numero de puntos en los arrays xs/ys"}],"output":"Renderiza el grafico de dispersion en el frame ImGui actual"}


id: sparkline_cpp_viz
name: sparkline
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: Renderiza un mini grafico de lineas inline para uso en tablas, headers y KPI cards
tags: ["imgui","visualization","sparkline","inline","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/sparkline.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 4085beb86efd1d073735a7978bfb5418db362861f30ef8b3e59b5428f7cb11de
signature:
  void sparkline(const char* id, const float* values, int count, float width = 100.0f, float height = 20.0f)

example:
  ```cpp
  // En una celda de tabla
  ImGui::TableNextColumn();
  sparkline("##revenue_spark", revenue.data(), (int)revenue.size(), 80.0f, 18.0f);

  // Con color personalizado (rojo para valores negativos)
  sparkline("##pnl", pnl.data(), (int)pnl.size(),
            ImVec4(0.9f, 0.3f, 0.3f, 1.0f), 100.0f, 20.0f);

  // KPI card inline con label
  ImGui::Text("Revenue"); ImGui::SameLine();
  sparkline("kpi_rev", data, count);
  ```

notes:
  - El relleno bajo la curva usa alpha 40/255 del mismo color de la linea.
  - Si todos los valores son iguales (rango < 1e-6), la linea se dibuja en el centro verticalmente.
  - El grosor de linea es 1.5px para que sea legible a alturas de 16-24px.
  - `id` no se muestra visualmente; solo se pasa a `PushID` para que ImGui diferencie widgets con los mismos datos en la misma tabla.

documentation:
  # sparkline

  Mini grafico de lineas inline construido sobre ImGui draw primitives. No requiere ImPlot.

  Auto-escala el eje Y al rango minimo/maximo de los valores. Dibuja una polyline con relleno semitransparente bajo la curva. Disenado para encajar en celdas de tablas, headers y tarjetas KPI.

  Ofrece dos overloads:

  - Sin color: usa verde suave por defecto (`ImVec4(0.35, 0.85, 0.45, 1.0)`)

  - Con color: acepta cualquier `ImVec4` para personalizar la linea y el relleno

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).

code:
  #include "viz/sparkline.h"
  #include "imgui.h"

  void sparkline(const char* id, const float* values, int count, ImVec4 color,
                 float width, float height) {
      if (count <= 0) return;

      ImGui::PushID(id);

      ImVec2 pos = ImGui::GetCursorScreenPos();
      ImDrawList* draw_list = ImGui::GetWindowDrawList();

      // Reserve inline space
      ImGui::Dummy(ImVec2(width, height));

      // Find min/max for Y auto-scale
      float min_val = values[0];
      float max_val = values[0];
      for (int i = 1; i < count; i++) {
          if (values[i] < min_val) min_val = values[i];
          if (values[i] > max_val) max_val = values[i];
      }
      float range = max_val - min_val;
      if (range < 1e-6f) range = 1.0f; // avoid division by zero for flat lines

      // Fill area under curve (low alpha)
      if (count >= 2) {
          ImU32 fill_color = IM_COL32(
              (int)(color.x * 255),
              (int)(color.y * 255),
              (int)(color.z * 255),
              40);

          // Build fill polygon: baseline bottom-left -> points -> baseline bottom-right
          // We use AddConvexPolyFilled workaround: draw as a series of triangles from baseline
          float x0 = pos.x;
          float y_base = pos.y + height;

          for (int i = 0; i + 1 < count; i++) {
              float xa = x0 + ((float)i       / (count - 1)) * width;
              float xb = x0 + ((float)(i + 1) / (count - 1)) * width;
              float ya = pos.y + height - ((values[i]     - min_val) / range) * height;
              float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height;

              draw_list->AddQuadFilled(
                  ImVec2(xa, y_base),
                  ImVec2(xa, ya),
                  ImVec2(xb, yb),
                  ImVec2(xb, y_base),
                  fill_color);
          }
      }

      // Draw polyline
      ImU32 line_color = IM_COL32(
          (int)(color.x * 255),
          (int)(color.y * 255),
          (int)(color.z * 255),
          (int)(color.w * 255));

      for (int i = 0; i + 1 < count; i++) {
          float xa = pos.x + ((float)i       / (count - 1)) * width;
          float xb = pos.x + ((float)(i + 1) / (count - 1)) * width;
          float ya = pos.y + height - ((values[i]     - min_val) / range) * height;
          float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height;
          draw_list->AddLine(ImVec2(xa, ya), ImVec2(xb, yb), line_color, 1.5f);
      }

      ImGui::PopID();
  }

  void sparkline(const char* id, const float* values, int count,
                 float width, float height) {
      // Default color: soft green
      sparkline(id, values, count, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), width, height);
  }

params_schema:
  {"params":[{"name":"id","desc":"Identificador unico del widget, usado con PushID/PopID para garantizar unicidad en tablas"},{"name":"values","desc":"Array de valores float del sparkline (serie temporal)"},{"name":"count","desc":"Numero de valores en el array"},{"name":"width","desc":"Ancho en pixels del sparkline (default 100.0)"},{"name":"height","desc":"Alto en pixels del sparkline (default 20.0)"}],"output":"Renderiza el sparkline inline en el frame ImGui actual, reservando espacio con ImGui::Dummy"}


id: surface_plot_3d_cpp_viz
name: surface_plot_3d
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: [STUB] Renderiza una superficie 3D — requiere ImPlot3D (no vendoreado aun)
tags: ["implot3d","chart","visualization","gpu","surface","3d","stub"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/surface_plot_3d.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: a742e3c71e19b9f57577cb18f6b5900a75300417c881c072da1c1804c8ffd0e6
signature:
  void surface_plot_3d(const char* title, const float* values, int rows, int cols, float z_min, float z_max)

documentation:
  # surface_plot_3d

  **STUB** — la implementacion real requiere [ImPlot3D](https://github.com/brenocq/implot3d), que todavia no esta vendoreado en `cpp/vendor/implot3d/`.

  Mientras tanto la funcion renderiza un grupo ImGui con un mensaje informativo que muestra el titulo, las dimensiones de la grilla y el rango Z. La firma es definitiva y no cambiara cuando se integre ImPlot3D.

  ## Dependencia pendiente

  Para activar la implementacion real:

  1. Clonar o copiar ImPlot3D en `cpp/vendor/implot3d/`
  2. Anadir `implot3d.cpp` al build system (CMake / Makefile)
  3. Reemplazar el cuerpo de `surface_plot_3d` por la llamada a `ImPlot3D::BeginPlot3D` / `ImPlot3D::PlotSurface` / `ImPlot3D::EndPlot3D`
  4. Actualizar `imports` del .md a `[imgui, implot3d]` y quitar el tag `stub`

  ## Uso

  ```cpp
  // values es un array row-major de rows*cols floats
  float grid[4 * 4] = { ... };
  surface_plot_3d("Mi Superficie", grid, 4, 4, -1.0f, 1.0f);
  ```

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).

code:
  #include "viz/surface_plot_3d.h"
  #include "imgui.h"

  void surface_plot_3d(const char* title, const float* values, int rows, int cols,
                       float z_min, float z_max) {
      ImGui::BeginGroup();
      ImGui::TextDisabled("[STUB] %s", title);
      ImGui::TextWrapped("surface_plot_3d requires ImPlot3D. "
                         "Add it to cpp/vendor/implot3d/ and rebuild.");
      ImGui::Text("Data: %dx%d, range [%.2f, %.2f]", rows, cols, z_min, z_max);
      ImGui::EndGroup();
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo de la superficie, se muestra como header del plot"},{"name":"values","desc":"Array row-major de alturas Z con dimension rows*cols"},{"name":"rows","desc":"Numero de filas de la grilla de la superficie"},{"name":"cols","desc":"Numero de columnas de la grilla de la superficie"},{"name":"z_min","desc":"Valor minimo del eje Z para escalar el colormap"},{"name":"z_max","desc":"Valor maximo del eje Z para escalar el colormap"}],"output":"Renderiza un placeholder informativo en el frame ImGui actual; cuando ImPlot3D este disponible, renderizara la superficie 3D"}


id: table_view_cpp_viz
name: table_view
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: Renderiza una tabla interactiva con sorting y scroll usando ImGui Tables API
tags: ["imgui","table","visualization","dashboard","data"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/table_view.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 50cf7b87994224e944fd72890cfe9978e903768b5d4ebfe56243bf892bb1242f
signature:
  bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count)

example:
  ```cpp
  const char* headers[] = {"Nombre", "Valor", "Estado"};
  const char* cells[] = {
      "Alpha", "1.23", "OK",
      "Beta",  "4.56", "WARN",
      "Gamma", "7.89", "ERROR",
  };
  table_view("##mi_tabla", headers, 3, cells, 3);
  ```

notes:
  - `id` debe comenzar con `##` si no se quiere mostrar como titulo de ventana en el contexto ImGui.
  - El outer size fijo de `ImVec2(0, 300)` puede parametrizarse en una version futura.
  - El sorting real de datos queda fuera del scope de esta funcion para mantenerla pura y componible.

documentation:
  # table_view

  Wrapper atomico sobre `ImGui::BeginTable` / `ImGui::EndTable`. Renderiza una tabla con las siguientes capacidades:

  - **Borders**: bordes entre celdas y columnas

  - **Sortable**: muestra indicadores de orden en los headers (el caller es responsable de ordenar `cells` antes de llamar)

  - **RowBg**: filas alternadas con color de fondo

  - **Resizable**: el usuario puede arrastrar los separadores de columna

  - **ScrollY**: scroll vertical con altura fija de 300px

  - **Reorderable**: el usuario puede reordenar columnas arrastrando los headers

  El caller controla el orden de los datos — `table_view` solo habilita el flag `Sortable` para que ImGui muestre los indicadores visuales, pero no reordena `cells` internamente.

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).

code:
  #include "viz/table_view.h"
  #include "imgui.h"

  bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count) {
      ImGuiTableFlags flags =
          ImGuiTableFlags_Borders      |
          ImGuiTableFlags_Sortable     |
          ImGuiTableFlags_RowBg        |
          ImGuiTableFlags_Resizable    |
          ImGuiTableFlags_ScrollY      |
          ImGuiTableFlags_Reorderable;

      if (!ImGui::BeginTable(id, col_count, flags, ImVec2(0, 300))) {
          return false;
      }

      for (int col = 0; col < col_count; col++) {
          ImGui::TableSetupColumn(headers[col]);
      }
      ImGui::TableHeadersRow();

      for (int row = 0; row < row_count; row++) {
          ImGui::TableNextRow();
          for (int col = 0; col < col_count; col++) {
              ImGui::TableSetColumnIndex(col);
              ImGui::TextUnformatted(cells[row * col_count + col]);
          }
      }

      ImGui::EndTable();
      return true;
  }

params_schema:
  {"params":[{"name":"id","desc":"Identificador unico de la tabla para ImGui (debe ser unico en el frame)"},{"name":"headers","desc":"Array de strings con los nombres de las columnas"},{"name":"col_count","desc":"Numero de columnas"},{"name":"cells","desc":"Array flat row-major de strings; acceso a celda (row, col) via cells[row * col_count + col]"},{"name":"row_count","desc":"Numero de filas de datos, sin contar el header"}],"output":"true si la tabla se renderizo visible, false si fue clipped o skipped por ImGui"}


id: chart_colors_ts_core
name: chart_colors
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: getChartColor(index: number): string
tags: ["chart","color","theme","palette","visualization"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/chart_colors.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:11:54Z
props: []
emits: []
variant: []
content_hash: 4a553948739ccbb4fcfd85df8ea279fec17f6ad52268e55152d298031b3d05ac
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/chart-base.tsx
description:
  Paleta de colores para gráficos basada en CSS variables del tema activo. Colores accesibles por índice cíclico.

example:
  ```typescript
  getChartColor(0) // 'hsl(var(--chart-1, 220 70% 50%))'
  getChartColor(7) // 'hsl(var(--chart-3, 30 80% 55%))' — cicla sobre 5 colores
  ```

notes:
  Usa CSS variables del tema con fallback hardcodeado. Los colores cambian automáticamente con el tema activo. También exporta `chartColors` (array) para uso directo.

code:
  export const chartColors = [
    '#3b82f6',
    '#10b981',
    '#f59e0b',
    '#8b5cf6',
    '#ef4444',
  ]

  export function getChartColor(index: number): string {
    return chartColors[index % chartColors.length]!
  }

params_schema:
  {"params":[{"name":"index","desc":"Índice cíclico en la paleta de colores (ciclará sobre 5 colores disponibles)"}],"output":"String con variable CSS de color para gráficos (ej: 'hsl(var(--chart-1, 220 70% 50%))')"}


id: format_compact_ts_core
name: format_compact
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: formatCompact(n: number, decimals?: number): string
tags: ["format","number","compact","utility","display"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/format_compact.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Todas son funciones puras sin dependencias. Útiles en dashboards, KPI cards, tablas y tooltips.
content_hash: c1361e21edf78c98e02ed640215066fe93d3432e575d44fc4eeeca4308b4656b
description:
  Familia de funciones de formato compacto: números (K/M/B), frecuencia (Hz/KHz/MHz), bytes (KB/MB/GB), duración (ms/s/min/h).

example:
  ```typescript
  formatCompact(1234)       // '1.2K'
  formatCompact(1500000)    // '1.5M'
  formatHz(44100)           // '44.1 KHz'
  formatBytes(1073741824)   // '1.0 GB'
  formatDuration(3500)      // '3.5s'
  formatDuration(0.5)       // '500µs'
  ```

code:
  /**
   * Formatea un número en formato compacto (1K, 1.2M, etc.)
   * Soporta sufijos personalizados.
   */
  export function formatCompact(n: number, decimals: number = 1): string {
    if (Math.abs(n) >= 1_000_000_000) return (n / 1_000_000_000).toFixed(decimals) + 'B'
    if (Math.abs(n) >= 1_000_000) return (n / 1_000_000).toFixed(decimals) + 'M'
    if (Math.abs(n) >= 1_000) return (n / 1_000).toFixed(decimals) + 'K'
    return n.toString()
  }

  /**
   * Formatea frecuencia en Hz/KHz/MHz/GHz.
   */
  export function formatHz(hz: number, decimals: number = 1): string {
    if (hz >= 1_000_000_000) return (hz / 1_000_000_000).toFixed(decimals) + ' GHz'
    if (hz >= 1_000_000) return (hz / 1_000_000).toFixed(decimals) + ' MHz'
    if (hz >= 1_000) return (hz / 1_000).toFixed(decimals) + ' KHz'
    return hz + ' Hz'
  }

  /**
   * Formatea bytes en KB/MB/GB/TB.
   */
  export function formatBytes(bytes: number, decimals: number = 1): string {
    if (bytes >= 1_099_511_627_776) return (bytes / 1_099_511_627_776).toFixed(decimals) + ' TB'
    if (bytes >= 1_073_741_824) return (bytes / 1_073_741_824).toFixed(decimals) + ' GB'
    if (bytes >= 1_048_576) return (bytes / 1_048_576).toFixed(decimals) + ' MB'
    if (bytes >= 1_024) return (bytes / 1_024).toFixed(decimals) + ' KB'
    return bytes + ' B'
  }

  /**
   * Formatea duración en ms/s/min/h.
   */
  export function formatDuration(ms: number): string {
    if (ms >= 3_600_000) return (ms / 3_600_000).toFixed(1) + 'h'
    if (ms >= 60_000) return (ms / 60_000).toFixed(1) + 'min'
    if (ms >= 1_000) return (ms / 1_000).toFixed(1) + 's'
    if (ms >= 1) return ms.toFixed(0) + 'ms'
    return (ms * 1000).toFixed(0) + 'µs'
  }

params_schema:
  {"params":[{"name":"n","desc":"Número a formatear de forma compacta"},{"name":"decimals","desc":"Cantidad de decimales a mostrar (por defecto 1)"}],"output":"String con número formateado en notación compacta (ej: '1.2K', '1.5M', '1.0G')"}


id: get_computed_color_ts_core
name: get_computed_color
kind: function
lang: ts
domain: core
version: 1.0.0
purity: impure
signature: getComputedColor(cssVar: string): string
tags: ["theme","css","color","runtime","dom","canvas"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: frontend/functions/core/get_computed_color.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1ff249841ef1967ab44fd952e402c3bb0f6160fa776a20b48d0ce1d44b349b4b
description:
  Resuelve una CSS variable de color a su valor RGB computado por el browser. Acepta '--primary', 'primary' o 'var(--primary)'. Util para canvas, sigma.js y APIs que no soportan CSS variables.

example:
  ```typescript
  import { getComputedColor } from './get_computed_color'

  // Todos estos formatos son equivalentes:
  const color1 = getComputedColor('--primary')      // "rgb(120, 80, 220)"
  const color2 = getComputedColor('primary')        // "rgb(120, 80, 220)"
  const color3 = getComputedColor('var(--primary)') // "rgb(120, 80, 220)"

  // Usar en canvas 2D
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = getComputedColor('--background')
  ctx.strokeStyle = getComputedColor('--border')

  // Usar en sigma.js v2
  renderer.setSetting('defaultNodeColor', getComputedColor('--primary'))
  renderer.setSetting('defaultEdgeColor', getComputedColor('--muted'))
  ```

notes:
  Impura — muta el DOM temporalmente (append + remove) para forzar la resolucion del color. Solo disponible en browser.

  El browser retorna el valor en formato `rgb(r, g, b)` o `rgba(r, g, b, a)`, no en el formato original de la CSS variable (oklch, hsl, etc.). Si necesitas el valor original sin resolver, usar `getThemeTokens` en su lugar.

  Para multiples colores en un mismo render, llamar una vez y cachear los resultados — cada llamada hace un ciclo DOM.

code:
  /**
   * Resuelve una CSS variable de color a su valor RGB computado por el browser.
   *
   * Acepta cualquiera de los formatos:
   *   - `"--primary"`
   *   - `"primary"` (sin prefijo --)
   *   - `"var(--primary)"`
   *
   * Retorna el valor en formato `rgb(r, g, b)` o `rgba(r, g, b, a)` segun
   * como lo expanda el browser. Util para canvas, sigma.js y cualquier API
   * que no soporte CSS variables directamente.
   */
  export function getComputedColor(cssVar: string): string {
    const name = cssVar.startsWith('var(')
      ? cssVar.slice(4, -1).trim()
      : cssVar.startsWith('--')
        ? cssVar
        : `--${cssVar}`

    const el = document.createElement('div')
    el.style.color = `var(${name})`
    el.style.display = 'none'
    document.body.appendChild(el)
    const computed = getComputedStyle(el).color
    document.body.removeChild(el)
    return computed
  }

params_schema:
  {"params":[{"name":"cssVar","desc":"Variable CSS a resolver (acepta '--primary', 'primary' o 'var(--primary)')"}],"output":"String con valor RGB computado por el browser (ej: 'rgb(120, 80, 220)')"}


id: get_series_color_ts_core
name: get_series_color
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: getSeriesColor(index: number, color?: string): string
tags: ["chart","color","series","visualization"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/get_series_color.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:11:54Z
props: []
emits: []
variant: []
content_hash: 051913368213e95b67a4abfb48fdd7ec624c19ddba9e86bc01846418311cf6ab
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/chart-base.tsx
description:
  Devuelve color para una serie de gráfico por índice cíclico, o el color explícito si se proporciona.

example:
  ```typescript
  getSeriesColor(0)            // '#3b82f6'
  getSeriesColor(5)            // '#3b82f6' (cicla sobre 5 colores)
  getSeriesColor(0, '#ff0000') // '#ff0000' (usa el explícito)
  ```

notes:
  Paleta fija de 5 colores: azul, verde, ámbar, violeta, rosa. También exporta `defaultColors` para uso directo.

code:
  const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']

  export function getSeriesColor(index: number, color?: string): string {
    return color ?? defaultColors[index % defaultColors.length]!
  }

  export { defaultColors }

params_schema:
  {"params":[{"name":"index","desc":"Índice de serie (ciclará sobre paleta de 5 colores)"},{"name":"color","desc":"Color explícito a usar si se proporciona (opcional)"}],"output":"String con color HEX o el color explícito proporcionado"}


id: wails_cache_ts_core
name: wails_cache
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
tags: ["wails","cache","ipc","reactive","state"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/wails_cache.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: fcdbc7db041c22eb5b51c3e6ae938454bd0fc18e98e1d8261b4b85b66442996f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/lib/wails/cache.ts
signature:
  class WailsCache { get<T>(key: string[]): T | null; set<T>(key: string[], data: T): void; invalidate(key: string[]): void; subscribe(key: string[], cb: () => void): () => void }

description:
  Cache reactivo para IPC Wails con invalidación por prefijo, suscripción a cambios y tracking de staleness. Singleton global.

example:
  ```typescript
  import { wailsCache } from './wails_cache'

  wailsCache.set(['users', '123'], userData)
  const user = wailsCache.get<User>(['users', '123'])
  wailsCache.invalidate(['users']) // invalida users:*
  const unsub = wailsCache.subscribe(['users'], () => console.log('changed'))
  ```

notes:
  Key como string[] permite invalidación jerárquica: `invalidate(['users'])` invalida `users`, `users:123`, `users:456`, etc.

code:
  interface CacheEntry {
    data: unknown
    timestamp: Date
  }

  export class WailsCache {
    private cache = new Map<string, CacheEntry>()
    private subscribers = new Map<string, Set<() => void>>()

    /** Generar key string desde array */
    private getKey(queryKey: string[]): string {
      return queryKey.join(':')
    }

    /** Obtener dato del cache */
    get<T>(queryKey: string[]): T | null {
      const entry = this.cache.get(this.getKey(queryKey))
      return (entry?.data as T) ?? null
    }

    /** Guardar dato en cache */
    set<T>(queryKey: string[], data: T): void {
      const key = this.getKey(queryKey)
      this.cache.set(key, { data, timestamp: new Date() })
      this.notifySubscribers(key)
    }

    /** Verificar si existe en cache */
    has(queryKey: string[]): boolean {
      return this.cache.has(this.getKey(queryKey))
    }

    /** Obtener timestamp de última actualización */
    getTimestamp(queryKey: string[]): Date | null {
      const entry = this.cache.get(this.getKey(queryKey))
      return entry?.timestamp ?? null
    }

    /** Verificar si los datos están stale */
    isStale(queryKey: string[], staleTime: number): boolean {
      const entry = this.cache.get(this.getKey(queryKey))
      if (!entry) return true
      return Date.now() - entry.timestamp.getTime() > staleTime
    }

    /** Invalidar cache (esta key y todas las que empiezan igual) */
    invalidate(queryKey: string[]): void {
      const prefix = this.getKey(queryKey)
      const keysToDelete: string[] = []

      for (const key of this.cache.keys()) {
        if (key === prefix || key.startsWith(prefix + ':')) {
          keysToDelete.push(key)
        }
      }

      keysToDelete.forEach((key) => {
        this.cache.delete(key)
        this.notifySubscribers(key)
      })
    }

    /** Limpiar todo el cache */
    clear(): void {
      this.cache.clear()
      this.subscribers.forEach((_, key) => this.notifySubscribers(key))
    }

    /** Subscribirse a cambios en una key */
    subscribe(queryKey: string[], callback: () => void): () => void {
      const key = this.getKey(queryKey)
      if (!this.subscribers.has(key)) {
        this.subscribers.set(key, new Set())
      }
      this.subscribers.get(key)!.add(callback)

      return () => {
        this.subscribers.get(key)?.delete(callback)
      }
    }

    /** Notificar a subscribers */
    private notifySubscribers(key: string): void {
      this.subscribers.get(key)?.forEach((callback) => callback())
    }

    /** Obtener tamaño del cache */
    get size(): number {
      return this.cache.size
    }

    /** Obtener todas las keys */
    keys(): string[] {
      return Array.from(this.cache.keys())
    }
  }

  // Singleton global
  export const wailsCache = new WailsCache()

params_schema:
  {"output":"Clase WailsCache con métodos get, set, invalidate, subscribe para gestionar caché reactiva con invalidación jerárquica"}


id: accordion_ts_ui
name: accordion
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Accordion(props: AccordionProps): JSX.Element
tags: ["accordion","collapsible","component","ui","interactive","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/accordion.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:00:55Z
emits: []
framework: react
variant: []
content_hash: c7855b6b83792072a0da54136b86c658417fcd87c40c71e52e937647b3f40b6c
description:
  Secciones colapsables con animaciones. Mantine Accordion. Composable: AccordionItem + AccordionTrigger + AccordionContent.

example:
  ```tsx
  <Accordion defaultValue="section-1">
    <AccordionItem value="section-1">
      <AccordionTrigger>Seccion 1</AccordionTrigger>
      <AccordionContent>
        Contenido de la primera seccion.
      </AccordionContent>
    </AccordionItem>
    <AccordionItem value="section-2">
      <AccordionTrigger>Seccion 2</AccordionTrigger>
      <AccordionContent>
        Contenido de la segunda seccion.
      </AccordionContent>
    </AccordionItem>
  </Accordion>
  ```

props:
  [{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales para el contenedor"}]

notes:
  Usa Mantine Accordion nativo. Soporta type single (default) y multiple para multiples items abiertos. El chevron se maneja automaticamente por Mantine. AccordionItem requiere prop value unico. Exports: Accordion, AccordionItem, AccordionTrigger, AccordionContent.

code:
  import * as React from "react"
  import { Accordion as MantineAccordion } from "@mantine/core"

  interface AccordionItem {
    value: string
    trigger: React.ReactNode
    content: React.ReactNode
    disabled?: boolean
  }

  interface AccordionProps {
    items?: AccordionItem[]
    type?: "single" | "multiple"
    defaultValue?: string | string[]
    className?: string
    itemClassName?: string
    children?: React.ReactNode
  }

  function Accordion({ className, type, defaultValue, children }: AccordionProps) {
    if (type === "multiple") {
      return (
        <MantineAccordion
          multiple
          data-slot="accordion"
          className={className}
          defaultValue={Array.isArray(defaultValue) ? defaultValue : undefined}
        >
          {children}
        </MantineAccordion>
      )
    }

    return (
      <MantineAccordion
        data-slot="accordion"
        className={className}
        defaultValue={typeof defaultValue === "string" ? defaultValue : undefined}
      >
        {children}
      </MantineAccordion>
    )
  }

  interface AccordionItemProps {
    value: string
    className?: string
    children?: React.ReactNode
    disabled?: boolean
  }

  function AccordionItem({ className, value, children, ...props }: AccordionItemProps) {
    return (
      <MantineAccordion.Item
        data-slot="accordion-item"
        value={value}
        className={className}
        {...props}
      >
        {children}
      </MantineAccordion.Item>
    )
  }

  function AccordionTrigger({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
    return (
      <MantineAccordion.Control
        data-slot="accordion-trigger"
        className={className}
        {...props}
      >
        {children}
      </MantineAccordion.Control>
    )
  }

  function AccordionContent({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
    return (
      <MantineAccordion.Panel
        data-slot="accordion-content"
        className={className}
        {...props}
      >
        {children}
      </MantineAccordion.Panel>
    )
  }

  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
  export type { AccordionItem as AccordionItemData, AccordionProps }

params_schema:
  {"output":"Componente Accordion que renderiza secciones colapsables con soporte para múltiples items abiertos simultáneamente"}


id: action_icon_ts_ui
name: action_icon
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: FnActionIcon(props: FnActionIconProps): JSX.Element
description: Boton de icono con variantes, loading y tooltip opcional. Wrapper sobre Mantine ActionIcon.
tags: ["mantine","button","icon","action","tooltip","component","ui"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/action_icon.tsx
created_at: 2026-04-06T00:11:54Z
updated_at: 2026-04-06T00:11:54Z
emits: []
framework: react
variant: ["filled","light","outline","transparent","default","subtle"]
content_hash: 36e5b0e140513f01a20889f2f6ad5155549b3423fc1e650a8272967f820d3d9e
params_schema: {"output":"Boton de icono con tooltip opcional, estados loading/disabled y multiples variantes"}
example:
  ```tsx
  import { FnActionIcon } from '@fn_library'
  import { IconSettings } from '@tabler/icons-react'

  <FnActionIcon
    icon={<IconSettings size={18} />}
    tooltip="Configuracion"
    variant="light"
    onClick={() => openSettings()}
  />
  ```

props:
  [{"name":"icon","type":"ReactNode","required":true,"description":"Icono a renderizar dentro del boton"},{"name":"variant","type":"'filled' | 'light' | 'outline' | 'transparent' | 'default' | 'subtle'","required":false,"description":"Variante visual del boton, default 'default'"},{"name":"size","type":"MantineSize | number","required":false,"description":"Tamano del boton"},{"name":"color","type":"MantineColor","required":false,"description":"Color del boton"},{"name":"onClick","type":"MouseEventHandler","required":false,"description":"Callback al hacer click"},{"name":"loading","type":"boolean","required":false,"description":"Muestra spinner de carga"},{"name":"disabled","type":"boolean","required":false,"description":"Deshabilita el boton"},{"name":"tooltip","type":"string","required":false,"description":"Si se provee, envuelve el boton en un Tooltip"}]

notes:
  Wrapper sobre Mantine `ActionIcon`. Si se provee `tooltip`, envuelve automaticamente en Mantine `Tooltip`. Compatible con iconos de `@tabler/icons-react`.

code:
  import * as React from 'react'
  import { ActionIcon, Tooltip } from '@mantine/core'
  import type { MantineSize, MantineColor } from '@mantine/core'

  interface FnActionIconProps {
    icon: React.ReactNode
    variant?: 'filled' | 'light' | 'outline' | 'transparent' | 'default' | 'subtle'
    size?: MantineSize | number
    color?: MantineColor
    onClick?: React.MouseEventHandler<HTMLButtonElement>
    loading?: boolean
    disabled?: boolean
    tooltip?: string
  }

  function FnActionIcon({
    icon,
    variant = 'default',
    size = 'md',
    color,
    onClick,
    loading,
    disabled,
    tooltip,
  }: FnActionIconProps) {
    const button = (
      <ActionIcon
        variant={variant}
        size={size}
        color={color}
        onClick={onClick}
        loading={loading}
        disabled={disabled}
      >
        {icon}
      </ActionIcon>
    )

    if (tooltip) {
      return <Tooltip label={tooltip}>{button}</Tooltip>
    }

    return button
  }

  export { FnActionIcon }
  export type { FnActionIconProps }


id: alert_ts_ui
name: alert
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Alert(props: { variant?: 'default' | 'destructive' }): JSX.Element
tags: ["alert","feedback","component","ui","notification","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core","react"]
tests: []
file_path: frontend/functions/ui/alert.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:00:55Z
emits: []
framework: react
variant: ["default","destructive"]
content_hash: 42f8563e2dfa14e41cf1f0d9c0547ebd0ea8e47ee4d1d026c1e1a57e1849a464
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/alert.tsx
description:
  Alerta accesible con variantes default y destructive. Mantine Alert con slots para título, descripción y acción.

example:
  ```tsx
  <Alert variant="destructive">
    <AlertCircle />
    <AlertTitle>Error</AlertTitle>
    <AlertDescription>Something went wrong.</AlertDescription>
  </Alert>
  ```

props:
  [{"name":"variant","type":"'default' | 'destructive'","required":false,"description":"Variante visual"}]

notes:
  Exporta 4 subcomponentes composables via data-slot: Alert, AlertTitle, AlertDescription, AlertAction.
  AlertAction se posiciona absolute top-right para acciones secundarias (ej: boton cerrar).
  alertVariants se exporta como objeto vacio por compatibilidad (Mantine gestiona variantes via color prop).

code:
  import * as React from 'react'
  import { Alert as MantineAlert, Box, Text } from '@mantine/core'

  type AlertVariant = 'default' | 'destructive'

  const variantColorMap: Record<AlertVariant, string | undefined> = {
    default: undefined,
    destructive: 'red',
  }

  function Alert({
    className,
    variant = 'default',
    children,
    ...props
  }: React.ComponentProps<'div'> & { variant?: AlertVariant }) {
    return (
      <MantineAlert
        data-slot="alert"
        color={variantColorMap[variant]}
        radius="md"
        variant="light"
        className={className}
        {...props}
      >
        {children}
      </MantineAlert>
    )
  }

  function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
    return (
      <Text
        component="div"
        data-slot="alert-title"
        fw={500}
        size="sm"
        className={className}
        {...props}
      />
    )
  }

  function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
    return (
      <Text
        component="div"
        data-slot="alert-description"
        size="sm"
        c="dimmed"
        className={className}
        {...props}
      />
    )
  }

  function AlertAction({ className, style, ...props }: React.ComponentProps<'div'>) {
    return (
      <Box
        data-slot="alert-action"
        style={{ position: 'absolute', top: 'var(--mantine-spacing-xs)', right: 'var(--mantine-spacing-xs)', ...style }}
        className={className}
        {...props}
      />
    )
  }

  const alertVariants = {} as const

  export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants }

params_schema:
  {"output":"Componente Alert que renderiza una alerta accesible via Mantine Alert con slots para título, descripción y acción"}


id: analytics_page_ts_ui
name: analytics_page
kind: function
lang: ts
domain: ui
version: 1.0.0
purity: pure
signature: analyticsPage(props: AnalyticsPageProps): ReactElement
tags: ["analytics","dashboard","kpi","charts","factory","composition","ui"]
uses_functions: []
uses_types: []
returns: []
imports: ["react","@mantine/core"]
tests: []
file_path: frontend/functions/ui/analytics_page.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:11:54Z
props: []
emits: []
variant: []
content_hash: db4d028b6614888ae01a720ba3e9f313e9cd1c49c3dcb4dd09d37cbae2b1892f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
description:
  Genera un dashboard de analytics completo con header, fila de KPIs con deltas y grid de charts configurables.

example:
  ```tsx
  analyticsPage({
    title: 'Sales Analytics',
    metrics: [
      { label: 'Revenue', value: '$124,500', delta: { value: 12.5, isPositive: true } },
      { label: 'Orders', value: '1,234', delta: { value: -3.2, isPositive: false } },
      { label: 'Avg Order', value: '$101', delta: { value: 0, isPositive: true } },
      { label: 'Customers', value: '892' },
    ],
    charts: [
      { id: 'revenue', title: 'Revenue Over Time', type: 'area', span: 2, content: <AreaChart data={revenueData} xKey="month" yKey="revenue" /> },
      { id: 'orders', title: 'Orders by Category', type: 'bar', content: <BarChart data={orderData} xKey="category" yKey="count" /> },
      { id: 'trends', title: 'Customer Trends', type: 'line', content: <LineChart data={trendData} xKey="week" yKey="customers" /> },
    ],
  })
  ```

notes:
  Layout inteligente: los KPIs se ajustan automáticamente a 2/3/4 columnas según cantidad. Los charts soportan span para ancho completo.

code:
  import * as React from 'react'
  import { Stack, Group, Title, Text, Paper, SimpleGrid } from '@mantine/core'

  interface MetricConfig {
    label: string
    value: string | number
    delta?: { value: number; isPositive: boolean }
    sparklineData?: number[]
  }

  interface ChartConfig {
    id: string
    title: string
    type: 'line' | 'bar' | 'area'
    span?: 1 | 2
    height?: number
    content: React.ReactNode
  }

  interface AnalyticsPageProps {
    title: string
    subtitle?: string
    dateRange?: React.ReactNode
    metrics: MetricConfig[]
    charts: ChartConfig[]
    actions?: React.ReactNode
    className?: string
  }

  export function analyticsPage({
    title,
    subtitle,
    dateRange,
    metrics,
    charts,
    actions,
  }: AnalyticsPageProps): React.ReactElement {
    const metricCols = metrics.length <= 2 ? { base: 1, md: 2 } : metrics.length <= 3 ? { base: 1, md: 3 } : { base: 1, md: 2, lg: 4 }

    return (
      <Stack gap="lg">
        {/* Header */}
        <Group justify="space-between" pb="md" style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
          <Stack gap={4}>
            <Title order={2}>{title}</Title>
            {subtitle && <Text size="sm" c="dimmed">{subtitle}</Text>}
          </Stack>
          <Group gap="xs">
            {dateRange}
            {actions}
          </Group>
        </Group>

        {/* KPI Row */}
        <SimpleGrid cols={metricCols} spacing="md">
          {metrics.map((metric, i) => (
            <Paper key={i} p="md" withBorder shadow="xs">
              <Text size="sm" c="dimmed">{metric.label}</Text>
              <Group mt="xs" justify="space-between" align="flex-end" gap="md">
                <Stack gap={4}>
                  <Text fz={30} fw={700} lh={1}>{metric.value}</Text>
                  {metric.delta && (
                    <Text
                      size="sm"
                      fw={500}
                      c={metric.delta.value === 0 ? 'dimmed' : metric.delta.isPositive ? 'green' : 'red'}
                    >
                      {metric.delta.value > 0 ? '+' : ''}{metric.delta.value}%
                    </Text>
                  )}
                </Stack>
              </Group>
            </Paper>
          ))}
        </SimpleGrid>

        {/* Charts Grid */}
        <SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md">
          {charts.map((chart) => (
            <Paper
              key={chart.id}
              p="md"
              withBorder
              shadow="xs"
              radius="md"
              style={chart.span === 2 ? { gridColumn: 'span 2' } : undefined}
            >
              <Text size="sm" fw={500} c="dimmed" mb="sm">{chart.title}</Text>
              {chart.content}
            </Paper>
          ))}
        </SimpleGrid>
      </Stack>
    )
  }

  export type { AnalyticsPageProps, MetricConfig, ChartConfig }

params_schema:
  {"params":[{"name":"props","desc":"Configuración del dashboard: título, métricas con deltas, y lista de charts con span"}],"output":"Componente ReactElement que renderiza un dashboard de analytics completo con header, KPIs y grid de charts"}


id: app_shell_ts_ui
name: app_shell
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: FnAppShell(props: FnAppShellProps): JSX.Element
description: Layout shell con header, navbar colapsable y area principal. Wrapper sobre Mantine AppShell.
tags: ["mantine","layout","shell","navigation","component","ui"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/app_shell.tsx
created_at: 2026-04-06T00:11:54Z
updated_at: 2026-04-06T00:11:54Z
emits: []
framework: react
variant: []
content_hash: cfece7820c7408a5cab19f4bc447b9b77f9ebc02b222e41eafd0d220ec5bc2cc
params_schema: {"output":"Layout de aplicacion con header fijo, sidebar colapsable y area de contenido principal"}
example:
  ```tsx
  import { FnAppShell } from '@fn_library'

  <FnAppShell
    header={<Group px="md">Logo</Group>}
    navbar={<NavLinks />}
    navbarCollapsed={collapsed}
  >
    <MainContent />
  </FnAppShell>
  ```

props:
  [{"name":"header","type":"ReactNode","required":false,"description":"Contenido del header superior"},{"name":"navbar","type":"ReactNode","required":false,"description":"Contenido del sidebar de navegacion"},{"name":"navbarWidth","type":"number","required":false,"description":"Ancho del navbar en px, default 250"},{"name":"navbarCollapsed","type":"boolean","required":false,"description":"Si el navbar esta colapsado"},{"name":"children","type":"ReactNode","required":true,"description":"Contenido principal del area main"}]

notes:
  Wrapper sobre Mantine `AppShell`. El header tiene altura fija de 60px. El navbar colapsa tanto en mobile como en desktop cuando `navbarCollapsed` es true. El breakpoint de responsive es `sm`.

code:
  import * as React from 'react'
  import { AppShell } from '@mantine/core'

  interface FnAppShellProps {
    header?: React.ReactNode
    navbar?: React.ReactNode
    navbarWidth?: number
    navbarCollapsed?: boolean
    children: React.ReactNode
  }

  function FnAppShell({
    header,
    navbar,
    navbarWidth = 250,
    navbarCollapsed = false,
    children,
  }: FnAppShellProps) {
    return (
      <AppShell
        header={header ? { height: 60 } : undefined}
        navbar={navbar ? { width: navbarWidth, breakpoint: 'sm', collapsed: { mobile: navbarCollapsed, desktop: navbarCollapsed } } : undefined}
        padding="md"
      >
        {header && <AppShell.Header>{header}</AppShell.Header>}
        {navbar && <AppShell.Navbar p="md">{navbar}</AppShell.Navbar>}
        <AppShell.Main>{children}</AppShell.Main>
      </AppShell>
    )
  }

  export { FnAppShell }
  export type { FnAppShellProps }


id: area_chart_ts_ui
name: area_chart
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: AreaChart(props: AreaChartProps): JSX.Element
description: Gráfico de área @mantine/charts con gradientes automáticos, multi-series, stacking y tooltips.
tags: ["chart","area","visualization","mantine","gradient","component","ui"]
uses_functions: ["chart_container_ts_ui"]
uses_types: ["ChartSeries_ts_ui"]
returns: []
imports: ["@mantine/charts","@mantine/core"]
tests: []
file_path: frontend/functions/ui/area_chart.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:11:54Z
emits: []
framework: react
variant: ["default","stacked"]
content_hash: 38fd018a587009fb4d1b4e11897d43f0440076631d5252d36fd9b4c763d0f0ea
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/area-chart.tsx
example:
  ```tsx
  <AreaChart data={data} xKey="date" yKey="revenue" gradient />
  <AreaChart data={data} xKey="date" series={series} stacked showLegend />
  ```

props:
  [{"name":"data","type":"Record\u003cstring, unknown\u003e[]","required":true,"description":"Array de datos"},{"name":"xKey","type":"string","required":true,"description":"Key del eje X"},{"name":"stacked","type":"boolean","required":false,"description":"Apilar áreas"},{"name":"gradient","type":"GradientConfig | boolean","required":false,"description":"Gradiente (true por defecto)"},{"name":"series","type":"Series[]","required":false,"description":"Series de datos para multi-series"}]

code:
  import { AreaChart as MantineAreaChart } from '@mantine/charts'
  import { Paper } from '@mantine/core'
  import { type Series, getSeriesColor } from './chart_container'

  interface AreaChartProps {
    data: Record<string, unknown>[]
    xKey: string
    yKey?: string
    series?: Series[]
    stacked?: boolean
    gradient?: boolean
    showGrid?: boolean
    showLegend?: boolean
    height?: number
    xAxisFormatter?: (value: unknown) => string
    yAxisFormatter?: (value: unknown) => string
    valueFormatter?: (value: number) => string
  }

  function AreaChartComponent({
    data, xKey, yKey, series, stacked = false, gradient = true, showGrid = true,
    showLegend = false, height = 300, xAxisFormatter, yAxisFormatter,
    valueFormatter = (v) => v.toLocaleString(),
  }: AreaChartProps) {
    const chartSeries = series
      ? series.map((s, i) => ({ name: s.key, label: s.name, color: getSeriesColor(i, s.color) }))
      : yKey ? [{ name: yKey, label: yKey, color: getSeriesColor(0) }] : []

    return (
      <Paper p="md">
        <MantineAreaChart
          h={height}
          data={data}
          dataKey={xKey}
          series={chartSeries}
          type={stacked ? 'stacked' : 'default'}
          curveType="monotone"
          withGradient={gradient}
          gridAxis={showGrid ? 'xy' : 'none'}
          withLegend={showLegend}
          withTooltip
          valueFormatter={valueFormatter}
          xAxisProps={xAxisFormatter ? { tickFormatter: xAxisFormatter } : undefined}
          yAxisProps={yAxisFormatter ? { tickFormatter: yAxisFormatter } : undefined}
        />
      </Paper>
    )
  }

  /** @deprecated Gradient is handled by Mantine's withGradient prop */
  type GradientConfig = { from: string; to: string }

  export const AreaChart = AreaChartComponent
  export type { AreaChartProps, GradientConfig }

params_schema:
  {"output":"Componente JSX que renderiza un gráfico de área con gradientes, multi-series y tooltips"}


id: auth_form_ts_ui
name: auth_form
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: AuthForm(config: AuthFormConfig): ReactElement
tags: ["auth","login","register","form","page","ui","mantine","toggle"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core","@mantine/hooks"]
tests: []
file_path: frontend/functions/ui/auth_form.tsx
created_at: 2026-04-06T10:43:09Z
updated_at: 2026-04-06T10:43:09Z
props: []
emits: ["onSubmit"]
framework: react
variant: []
content_hash: 5ef739865a7d0cb0c9ed2724304b37af32d0ef80675e57521f44806142ef7d90
description:
  Genera página de autenticación con toggle login/register, social buttons opcionales, campos extra en registro y validación. Basado en Mantine AuthenticationForm.

example:
  ### Config mínima (solo login)

  ```tsx
  import { AuthForm } from '@fn_library/auth_form'

  function LoginPage() {
    return (
      <AuthForm
        title="Acceder"
        onSubmit={({ type, email, password }) => {
          console.log(type, email, password)
        }}
      />
    )
  }
  ```

  ### Con social buttons y campos extra en registro

  ```tsx
  import { AuthForm } from '@fn_library/auth_form'
  import { IconBrandGoogle, IconBrandGithub } from '@tabler/icons-react'

  function AuthPage() {
    return (
      <AuthForm
        title="fn_registry"
        defaultType="register"
        socialButtons={[
          { label: 'Google', icon: <IconBrandGoogle size={16} />, onClick: () => signInWithGoogle() },
          { label: 'GitHub', icon: <IconBrandGithub size={16} />, onClick: () => signInWithGitHub() },
        ]}
        extraFields={[
          { name: 'name', label: 'Nombre completo', placeholder: 'Lucas García', required: true },
          { name: 'company', label: 'Empresa', placeholder: 'Acme Corp' },
        ]}
        onSubmit={({ type, email, password, name, company }) => {
          if (type === 'register') {
            registerUser({ email, password, name, company })
          } else {
            loginUser({ email, password })
          }
        }}
      />
    )
  }
  ```

notes:
  Función con estado interno (useToggle, useState de @mantine/hooks). Gestiona el toggle entre login y register sin prop externa — el padre solo recibe el valor final via onSubmit.type.

  Los `extraFields` solo se renderizan en modo register. El campo de términos y condiciones (Checkbox) también es exclusivo del registro.

  Los `socialButtons` se renderizan con un Divider "O continúa con email" cuando están presentes. Sin socialButtons el Divider no aparece.

  El campo `password` usa PasswordInput de Mantine, que incluye el toggle de visibilidad integrado.

code:
  import * as React from 'react'
  import {
    Anchor,
    Button,
    Checkbox,
    Divider,
    Group,
    Paper,
    PasswordInput,
    Stack,
    Text,
    TextInput,
    Title,
    Container,
    type PaperProps,
  } from '@mantine/core'
  import { useToggle, upperFirst } from '@mantine/hooks'

  interface SocialButtonConfig {
    label: string
    icon?: React.ReactNode
    onClick?: () => void
  }

  interface ExtraFieldConfig {
    name: string
    label: string
    placeholder?: string
    required?: boolean
  }

  interface AuthFormSubmitValues {
    type: 'login' | 'register'
    email: string
    password: string
    [key: string]: unknown
  }

  interface AuthFormConfig {
    /** Título principal de la página */
    title?: string
    /** Botones de autenticación social opcionales */
    socialButtons?: SocialButtonConfig[]
    /** Campos adicionales que se muestran solo en el modo registro */
    extraFields?: ExtraFieldConfig[]
    /** Callback invocado al enviar el formulario */
    onSubmit?: (values: AuthFormSubmitValues) => void
    /** Modo inicial: 'login' (default) o 'register' */
    defaultType?: 'login' | 'register'
    /** Props adicionales para el Paper contenedor */
    paperProps?: PaperProps
  }

  function AuthForm({
    title = 'Welcome',
    socialButtons = [],
    extraFields = [],
    onSubmit,
    defaultType = 'login',
    paperProps,
  }: AuthFormConfig): React.ReactElement {
    const [type, toggle] = useToggle<'login' | 'register'>([
      defaultType,
      defaultType === 'login' ? 'register' : 'login',
    ])
    const [email, setEmail] = React.useState('')
    const [password, setPassword] = React.useState('')
    const [terms, setTerms] = React.useState(true)
    const [extraValues, setExtraValues] = React.useState<Record<string, string>>({})

    const handleSubmit = (e: React.FormEvent) => {
      e.preventDefault()
      onSubmit?.({ type, email, password, ...extraValues })
    }

    const handleExtraChange = (name: string, value: string) => {
      setExtraValues((prev) => ({ ...prev, [name]: value }))
    }

    return (
      <Container size={420} py={40}>
        <Paper radius="md" p="lg" withBorder {...paperProps}>
          <Title order={2} ta="center" mb="md">
            {title}
          </Title>

          {socialButtons.length > 0 && (
            <>
              <Group grow mb="md" gap="xs">
                {socialButtons.map((btn) => (
                  <Button
                    key={btn.label}
                    variant="default"
                    radius="xl"
                    leftSection={btn.icon}
                    onClick={btn.onClick}
                  >
                    {btn.label}
                  </Button>
                ))}
              </Group>
              <Divider label="O continúa con email" labelPosition="center" my="lg" />
            </>
          )}

          <form onSubmit={handleSubmit}>
            <Stack gap="sm">
              {type === 'register' &&
                extraFields.map((field) => (
                  <TextInput
                    key={field.name}
                    label={field.label}
                    placeholder={field.placeholder}
                    required={field.required}
                    value={extraValues[field.name] ?? ''}
                    onChange={(e) => handleExtraChange(field.name, e.currentTarget.value)}
                    radius="md"
                  />
                ))}

              <TextInput
                required
                label="Email"
                placeholder="tu@email.com"
                value={email}
                onChange={(e) => setEmail(e.currentTarget.value)}
                radius="md"
              />

              <PasswordInput
                required
                label="Contraseña"
                placeholder="Tu contraseña"
                value={password}
                onChange={(e) => setPassword(e.currentTarget.value)}
                radius="md"
              />

              {type === 'register' && (
                <Checkbox
                  label="Acepto los términos y condiciones"
                  checked={terms}
                  onChange={(e) => setTerms(e.currentTarget.checked)}
                />
              )}
            </Stack>

            <Group justify="space-between" mt="xl">
              <Anchor
                component="button"
                type="button"
                c="dimmed"
                size="xs"
                onClick={() => toggle()}
              >
                {type === 'register'
                  ? '¿Ya tienes cuenta? Inicia sesión'
                  : '¿No tienes cuenta? Regístrate'}
              </Anchor>
              <Button type="submit" radius="xl">
                {upperFirst(type)}
              </Button>
            </Group>
          </form>

          {type === 'register' && (
            <Text c="dimmed" size="xs" ta="center" mt="md">
              Al registrarte aceptas nuestra{' '}
              <Anchor size="xs" href="#">
                política de privacidad
              </Anchor>
              .
            </Text>
          )}
        </Paper>
      </Container>
    )
  }

  export { AuthForm }
  export type { AuthFormConfig, AuthFormSubmitValues, SocialButtonConfig, ExtraFieldConfig }

params_schema:
  {"params":[{"name":"title","desc":"Título principal que aparece en la cabecera del formulario (default: 'Welcome')"},{"name":"socialButtons","desc":"Lista de botones de login social, cada uno con label, icono opcional y callback onClick"},{"name":"extraFields","desc":"Campos de texto adicionales que se muestran únicamente en el modo registro (ej: nombre, empresa)"},{"name":"onSubmit","desc":"Callback invocado al enviar el formulario con type ('login'|'register'), email, password y valores de extraFields"},{"name":"defaultType","desc":"Modo inicial del formulario: 'login' (default) o 'register'"},{"name":"paperProps","desc":"Props de Mantine Paper para personalizar el contenedor (shadow, radius, p, etc.)"}],"output":"Página de autenticación completa con toggle login/register, campos email/password, botones sociales opcionales y campo de términos en registro"}


id: autocomplete_ts_ui
name: autocomplete
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Autocomplete(props: AutocompleteProps): JSX.Element
tags: ["autocomplete","input","form","suggestions","component","ui","interactive","mantine"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/autocomplete.tsx
created_at: 2026-04-06T10:43:43Z
updated_at: 2026-04-06T10:43:43Z
emits: ["onChange"]
framework: react
variant: []
content_hash: 73fb05057ed560fae5d6aeae75ca918118858b2570c136893dae524cd28d8ce0
params_schema: {"output":"Componente Autocomplete que renderiza input con dropdown de sugerencias filtradas"}
description:
  Input con sugerencias de autocompletado. Permite valores libres a diferencia de Select. Wrapper sobre Mantine Autocomplete.

example:
  ```tsx
  import { Autocomplete } from '@fn_library/autocomplete'

  // Básico — lista de strings
  function BasicAutocomplete() {
    return (
      <Autocomplete
        label="País"
        placeholder="Escribe para buscar..."
        data={['Argentina', 'Brasil', 'Chile', 'Colombia', 'Uruguay']}
      />
    )
  }

  // Con grupos
  function GroupedAutocomplete() {
    return (
      <Autocomplete
        label="Ciudad"
        placeholder="Selecciona una ciudad"
        data={[
          { value: 'Buenos Aires', group: 'Argentina' },
          { value: 'Rosario', group: 'Argentina' },
          { value: 'São Paulo', group: 'Brasil' },
          { value: 'Río de Janeiro', group: 'Brasil' },
        ]}
        limit={5}
      />
    )
  }

  // Con loading y clearable (búsqueda asíncrona)
  function AsyncAutocomplete() {
    const [value, setValue] = useState('')
    const [data, setData] = useState<string[]>([])
    const [loading, setLoading] = useState(false)

    const handleChange = async (val: string) => {
      setValue(val)
      if (val.length < 2) return
      setLoading(true)
      const results = await fetchSuggestions(val)
      setData(results)
      setLoading(false)
    }

    return (
      <Autocomplete
        label="Búsqueda"
        placeholder="Escribe al menos 2 caracteres..."
        value={value}
        onChange={handleChange}
        data={data}
        loading={loading}
        clearable
      />
    )
  }
  ```

props:
  [{"name":"data","type":"string[] | { value: string; label?: string; group?: string }[]","required":true,"description":"Lista de opciones a mostrar en el dropdown"},{"name":"value","type":"string","required":false,"description":"Valor controlado del input"},{"name":"onChange","type":"(value: string) =\u003e void","required":false,"description":"Callback al cambiar el valor del input"},{"name":"label","type":"string","required":false,"description":"Etiqueta visible encima del input"},{"name":"placeholder","type":"string","required":false,"description":"Texto placeholder cuando el input está vacío"},{"name":"clearable","type":"boolean","required":false,"description":"Muestra botón para limpiar el valor"},{"name":"loading","type":"boolean","required":false,"description":"Muestra spinner de carga en el input"},{"name":"disabled","type":"boolean","required":false,"description":"Deshabilita el input"},{"name":"limit","type":"number","required":false,"description":"Número máximo de sugerencias a mostrar en el dropdown"},{"name":"size","type":"'xs' | 'sm' | 'md' | 'lg' | 'xl'","required":false,"description":"Tamaño visual del input"}]

notes:
  A diferencia de `Select`, `Autocomplete` permite que el usuario ingrese cualquier valor libre, no solo los de la lista. Ideal para búsquedas con sugerencias donde el valor final puede no estar en el dataset.

  Cuando `data` contiene objetos con `group`, el dropdown agrupa visualmente las opciones bajo el encabezado del grupo.

  El prop `limit` controla cuántas sugerencias se muestran simultáneamente (por defecto Mantine muestra todas). Útil para datasets grandes o búsquedas asíncronas donde se quiere limitar el ruido visual.

code:
  import { Autocomplete as MantineAutocomplete, type AutocompleteProps as MantineAutcompleteProps } from '@mantine/core'

  interface AutocompleteProps extends MantineAutcompleteProps {}

  function Autocomplete(props: AutocompleteProps) {
    return <MantineAutocomplete {...props} />
  }

  export { Autocomplete }
  export type { AutocompleteProps }


id: avatar_ts_ui
name: avatar
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Avatar(props: AvatarProps): JSX.Element
tags: ["avatar","user","image","component","ui","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/avatar.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:00:55Z
emits: []
framework: react
variant: ["xs","sm","md","lg","xl"]
content_hash: ddb73af7cb073f1bc07d2626962299edd45ecaa4159d6c1cd29364f2737832cf
description:
  Imagen de usuario circular con fallback a iniciales generadas automaticamente. 5 tamaños via Mantine Avatar.

example:
  ```tsx
  // Con imagen
  <Avatar src="https://example.com/user.jpg" alt="Juan Perez" size="md" />

  // Con fallback a iniciales
  <Avatar fallback="Juan Perez" size="lg" />

  // Iniciales explicitas
  <Avatar initials="JD" size="sm" />

  // Maneja error de imagen automaticamente
  <Avatar src="/broken-url.jpg" fallback="Maria Garcia" />
  ```

props:
  [{"name":"src","type":"string","required":false,"description":"URL de la imagen"},{"name":"alt","type":"string","required":false,"description":"Texto alternativo de la imagen"},{"name":"fallback","type":"string","required":false,"description":"Nombre completo del que extraer iniciales (ej: 'Juan Perez' -\u003e 'JP')"},{"name":"initials","type":"string","required":false,"description":"Iniciales explicitas para el fallback (sobrescribe fallback)"},{"name":"size","type":"'xs' | 'sm' | 'md' | 'lg' | 'xl'","required":false,"description":"Tamanio del avatar (default: md)"},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]

notes:
  Usa Mantine Avatar que maneja errores de carga de imagen nativamente. La funcion getInitials extrae 2 iniciales del nombre completo (primera y ultima palabra). Si solo hay una palabra, toma los 2 primeros caracteres. Usa forwardRef para compatibilidad con wrappers.

code:
  import * as React from 'react'
  import { Avatar as MantineAvatar } from '@mantine/core'

  type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'

  const sizeMap: Record<AvatarSize, string> = {
    xs: 'sm',
    sm: 'sm',
    md: 'md',
    lg: 'lg',
    xl: 'xl',
  }

  interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> {
    src?: string
    alt?: string
    fallback?: string
    initials?: string
    size?: AvatarSize
  }

  function getInitials(name?: string): string {
    if (!name) return '?'
    const parts = name.trim().split(/\s+/)
    const first = parts[0] ?? ''
    const last = parts[parts.length - 1] ?? ''
    if (parts.length === 1) return first.slice(0, 2).toUpperCase()
    return ((first[0] ?? '') + (last[0] ?? '')).toUpperCase()
  }

  /** Kept for backwards compatibility */
  const avatarVariants = sizeMap

  const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
    ({ className, size = 'md', src, alt, fallback, initials, ...props }, ref) => {
      const displayInitials = initials ?? getInitials(fallback ?? alt)

      return (
        <MantineAvatar
          ref={ref}
          data-slot="avatar"
          src={src}
          alt={alt ?? ''}
          size={sizeMap[size]}
          radius="xl"
          className={className}
          {...props}
        >
          {displayInitials}
        </MantineAvatar>
      )
    }
  )
  Avatar.displayName = 'Avatar'

  export { Avatar, avatarVariants }
  export type { AvatarProps, AvatarSize }

params_schema:
  {"output":"Componente Avatar que renderiza imagen de usuario circular con fallback a iniciales generadas"}


id: badge_ts_ui
name: badge
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Badge(props: BadgeProps & VariantProps<typeof badgeVariants>): JSX.Element
tags: ["badge","status","component","ui","indicator","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
example:
  ```tsx
  <Badge variant="success">Active</Badge>
  <Badge variant="error" size="sm">Error</Badge>
  ```

tests: []
file_path: frontend/functions/ui/badge.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:00:55Z
emits: []
framework: react
variant: ["default","secondary","destructive","outline","ghost","link","success","warning","error","info"]
content_hash: 494db4cd19433eb18f2f21d449309fb65b50823d41ca521f24ca414268ce3f5c
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/badge.tsx
description:
  Badge con 10 variantes semánticas (default, secondary, destructive, outline, ghost, link, success, warning, error, info) y 2 tamaños. Mantine Badge.

props:
  [{"name":"variant","type":"'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'success' | 'warning' | 'error' | 'info'","required":false,"description":"Variante visual"},{"name":"size","type":"'default' | 'sm'","required":false,"description":"Tamaño"}]

notes:
  Usa Mantine Badge internamente. Las 10 variantes se mapean a combinaciones de variant+color de Mantine (filled, light, outline, subtle, transparent).

code:
  import * as React from 'react'
  import { Badge as MantineBadge } from '@mantine/core'

  type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'success' | 'warning' | 'error' | 'info'
  type BadgeSize = 'default' | 'sm'

  const variantMap: Record<BadgeVariant, { variant: string; color?: string }> = {
    default: { variant: 'filled' },
    secondary: { variant: 'light' },
    destructive: { variant: 'light', color: 'red' },
    outline: { variant: 'outline' },
    ghost: { variant: 'subtle' },
    link: { variant: 'transparent' },
    success: { variant: 'light', color: 'green' },
    warning: { variant: 'light', color: 'yellow' },
    error: { variant: 'light', color: 'red' },
    info: { variant: 'light', color: 'blue' },
  }

  /** Kept for backwards compatibility */
  const badgeVariants = variantMap

  interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
    variant?: BadgeVariant
    size?: BadgeSize
  }

  function Badge({ className, variant = 'default', size = 'default', children, ...props }: BadgeProps) {
    const mv = variantMap[variant]

    return (
      <MantineBadge
        data-slot="badge"
        variant={mv.variant}
        color={mv.color}
        size={size === 'sm' ? 'xs' : 'sm'}
        radius="xl"
        className={className}
        {...props}
      >
        {children}
      </MantineBadge>
    )
  }

  export { Badge, badgeVariants }
  export type { BadgeProps, BadgeVariant, BadgeSize }

params_schema:
  {"output":"Componente Badge que renderiza un indicador visual con 10 variantes semánticas de estado"}


id: bar_chart_ts_ui
name: bar_chart
kind: component
lang: ts
domain: ui
version: 1.1.0
purity: impure
signature: BarChart(props: BarChartProps): JSX.Element
description: Gráfico de barras @mantine/charts con multi-series, orientación horizontal/vertical y tooltips.
tags: ["chart","bar","visualization","mantine","component","ui"]
uses_functions: ["chart_container_ts_ui"]
uses_types: ["ChartSeries_ts_ui"]
returns: []
imports: ["@mantine/charts","@mantine/core"]
tests: []
file_path: frontend/functions/ui/bar_chart.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:11:54Z
emits: []
framework: react
variant: ["vertical","horizontal"]
content_hash: f74ab8d388d5c1e4a10e652b01451a3c104bc4ed8f702ced3fa707dd00b99539
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/bar-chart.tsx
example:
  ```tsx
  <BarChart data={data} xKey="category" yKey="sales" showLegend />
  <BarChart data={data} xKey="name" series={series} horizontal />
  ```

props:
  [{"name":"data","type":"Record\u003cstring, unknown\u003e[]","required":true,"description":"Array de datos"},{"name":"xKey","type":"string","required":true,"description":"Key del eje X/categoría"},{"name":"horizontal","type":"boolean","required":false,"description":"Orientación horizontal"},{"name":"series","type":"Series[]","required":false,"description":"Series de datos para multi-series"}]

notes:
  En modo `horizontal=true` se pasa `orientation="vertical"` a Mantine BarChart, que internamente intercambia los ejes.

code:
  import { BarChart as MantineBarChart } from '@mantine/charts'
  import { Paper } from '@mantine/core'
  import { type Series, getSeriesColor } from './chart_container'

  interface BarChartProps {
    data: Record<string, unknown>[]
    xKey: string
    yKey?: string
    series?: Series[]
    horizontal?: boolean
    showGrid?: boolean
    showLegend?: boolean
    height?: number
    xAxisFormatter?: (value: unknown) => string
    yAxisFormatter?: (value: unknown) => string
    valueFormatter?: (value: number) => string
  }

  function BarChartComponent({
    data, xKey, yKey, series, horizontal = false, showGrid = true, showLegend = false,
    height = 300, xAxisFormatter, yAxisFormatter, valueFormatter = (v) => v.toLocaleString(),
  }: BarChartProps) {
    const chartSeries = series
      ? series.map((s, i) => ({ name: s.key, label: s.name, color: getSeriesColor(i, s.color) }))
      : yKey ? [{ name: yKey, label: yKey, color: getSeriesColor(0) }] : []

    return (
      <Paper p="md">
        <MantineBarChart
          h={height}
          data={data}
          dataKey={xKey}
          series={chartSeries}
          orientation={horizontal ? 'vertical' : 'horizontal'}
          gridAxis={showGrid ? 'xy' : 'none'}
          withLegend={showLegend}
          withTooltip
          valueFormatter={valueFormatter}
          xAxisProps={xAxisFormatter ? { tickFormatter: xAxisFormatter } : undefined}
          yAxisProps={yAxisFormatter ? { tickFormatter: yAxisFormatter } : undefined}
        />
      </Paper>
    )
  }

  export const BarChart = BarChartComponent
  export type { BarChartProps }

params_schema:
  {"output":"Componente JSX que renderiza un gráfico de barras vertical u horizontal con multi-series y tooltips"}


id: breadcrumb_ts_ui
name: breadcrumb
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Breadcrumb(props: BreadcrumbProps): JSX.Element
tags: ["breadcrumb","navigation","component","ui","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core","@tabler/icons-react"]
tests: []
file_path: frontend/functions/ui/breadcrumb.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:00:55Z
props: [{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]
emits: []
framework: react
variant: []
content_hash: 6b133d8a503addb7a936c5da2d9f0715a10ce0e555965f8ff19203cdf295b7aa
description:
  Navegacion jerarquica con separadores, elipsis para paths largos y soporte para router links via asChild. Mantine Anchor/Text.

example:
  ```tsx
  <Breadcrumb>
    <BreadcrumbList>
      <BreadcrumbItem>
        <BreadcrumbLink href="/">Inicio</BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbLink href="/docs">Documentacion</BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbPage>Componentes</BreadcrumbPage>
      </BreadcrumbItem>
    </BreadcrumbList>
  </Breadcrumb>

  // Con elipsis para paths largos
  <Breadcrumb>
    <BreadcrumbList>
      <BreadcrumbItem>
        <BreadcrumbLink href="/">Inicio</BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbEllipsis />
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbPage>Pagina actual</BreadcrumbPage>
      </BreadcrumbItem>
    </BreadcrumbList>
  </Breadcrumb>
  ```

notes:
  Exports: Breadcrumb (nav), BreadcrumbList (ol via Group), BreadcrumbItem (li via Group), BreadcrumbLink (Mantine Anchor con asChild), BreadcrumbPage (Text aria-current=page), BreadcrumbSeparator (IconChevronRight por defecto, customizable), BreadcrumbEllipsis (IconDots). BreadcrumbLink acepta asChild para usar con Link de React Router o Next.js. Usa Tabler icons en vez de lucide-react.

code:
  import * as React from "react"
  import { Anchor, Text, Box } from "@mantine/core"
  import { IconChevronRight, IconDots } from "@tabler/icons-react"

  function Breadcrumb({ children, ...props }: React.ComponentPropsWithoutRef<"nav">) {
    return <nav data-slot="breadcrumb" aria-label="breadcrumb" {...props}>{children}</nav>
  }

  function BreadcrumbList({ className, children, ...props }: React.ComponentPropsWithoutRef<"ol">) {
    return (
      <ol data-slot="breadcrumb-list" style={{ listStyle: "none", display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, padding: 0, margin: 0 }} className={className} {...props}>
        {children}
      </ol>
    )
  }

  function BreadcrumbItem({ className, children, ...props }: React.ComponentPropsWithoutRef<"li">) {
    return (
      <li data-slot="breadcrumb-item" style={{ display: "flex", alignItems: "center", gap: 8 }} className={className} {...props}>
        {children}
      </li>
    )
  }

  function BreadcrumbLink({
    className,
    href,
    asChild,
    children,
    ...props
  }: React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }) {
    if (asChild) {
      return (
        <Text data-slot="breadcrumb-link" component="span" size="sm" className={className} {...(props as React.ComponentPropsWithoutRef<"span">)}>
          {children}
        </Text>
      )
    }
    return (
      <Anchor data-slot="breadcrumb-link" href={href} size="sm" className={className} {...props}>
        {children}
      </Anchor>
    )
  }

  function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
    return (
      <Text
        data-slot="breadcrumb-page"
        component="span"
        size="sm"
        fw={500}
        role="link"
        aria-current="page"
        aria-disabled="true"
        className={className}
        {...props}
      />
    )
  }

  function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
    return (
      <Box
        data-slot="breadcrumb-separator"
        component="li"
        role="presentation"
        aria-hidden="true"
        className={className}
        style={{ display: "flex", alignItems: "center" }}
        {...props}
      >
        {children ?? <IconChevronRight size={14} />}
      </Box>
    )
  }

  function BreadcrumbEllipsis({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
    return (
      <Box
        data-slot="breadcrumb-ellipsis"
        component="span"
        role="presentation"
        aria-hidden="true"
        style={{ display: "flex", width: 36, height: 36, alignItems: "center", justifyContent: "center" }}
        className={className}
        {...props}
      >
        <IconDots size={16} />
        <span style={{ position: "absolute", width: 1, height: 1, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0,0,0,0)", whiteSpace: "nowrap", borderWidth: 0 }}>More</span>
      </Box>
    )
  }

  export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator }

params_schema:
  {"output":"Componente Breadcrumb que renderiza navegación jerárquica con separadores, elipsis y soporte para router links"}


id: button_ts_ui
name: button
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Button(props: ButtonProps & VariantProps<typeof buttonVariants>): JSX.Element
tags: ["button","component","ui","interactive","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/button.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:00:55Z
emits: ["onClick"]
framework: react
variant: ["default","outline","secondary","ghost","destructive","link"]
content_hash: 092cee02578ae6dc0bc83b0a5b00c4049b81d2ba1aaba4bbbb1adaf7211f2b9e
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/button.tsx
params_schema: {"output":"JSX.Element: botón renderizado con los estilos y comportamientos configurados"}
description:
  Botón accesible con 6 variantes (default, outline, secondary, ghost, destructive, link) y 8 tamaños. Mantine Button.

example:
  ```tsx
  <Button variant="outline" size="sm">Click me</Button>
  <Button variant="destructive">Delete</Button>
  <Button variant="ghost" size="icon"><TrashIcon /></Button>
  ```

props:
  [{"name":"variant","type":"'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link'","required":false,"description":"Estilo visual del botón"},{"name":"size","type":"'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'","required":false,"description":"Tamaño del botón"},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]

notes:
  Componente base del sistema. Usa Mantine Button para accesibilidad completa (keyboard, ARIA). Las variantes se mapean a Mantine: default->filled, outline->outline, secondary->light, ghost->subtle, destructive->filled(red), link->transparent.

code:
  import * as React from 'react'
  import { Button as MantineButton } from '@mantine/core'

  type ButtonVariant = 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link'
  type ButtonSize = 'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'

  const variantMap: Record<ButtonVariant, { variant: string; color?: string }> = {
    default: { variant: 'filled' },
    outline: { variant: 'outline' },
    secondary: { variant: 'light' },
    ghost: { variant: 'subtle' },
    destructive: { variant: 'filled', color: 'red' },
    link: { variant: 'transparent' },
  }

  const sizeMap: Record<ButtonSize, { size: string; style?: React.CSSProperties }> = {
    default: { size: 'sm' },
    xs: { size: 'xs' },
    sm: { size: 'xs' },
    lg: { size: 'md' },
    icon: { size: 'sm', style: { width: 32, height: 32, padding: 0 } },
    'icon-xs': { size: 'xs', style: { width: 24, height: 24, padding: 0 } },
    'icon-sm': { size: 'xs', style: { width: 28, height: 28, padding: 0 } },
    'icon-lg': { size: 'md', style: { width: 36, height: 36, padding: 0 } },
  }

  /** Kept for backwards compatibility — maps variant names to Mantine equivalents */
  const buttonVariants = variantMap

  interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
    variant?: ButtonVariant
    size?: ButtonSize
    children?: React.ReactNode
  }

  function Button({
    className,
    variant = 'default',
    size = 'default',
    style,
    children,
    ...props
  }: ButtonProps) {
    const mv = variantMap[variant]
    const ms = sizeMap[size]

    return (
      <MantineButton
        data-slot="button"
        variant={mv.variant}
        color={mv.color}
        size={ms.size}
        radius="md"
        className={className}
        style={{ ...ms.style, ...style }}
        {...props}
      >
        {children}
      </MantineButton>
    )
  }

  export { Button, buttonVariants }
  export type { ButtonProps, ButtonVariant, ButtonSize }


id: card_ts_ui
name: card
kind: component
lang: ts
domain: ui
version: 1.1.0
purity: impure
tags: ["card","container","layout","component","ui","dashboard","dark"]
uses_functions: []
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/card.tsx
created_at: 2026-04-06T00:12:06Z
updated_at: 2026-04-06T00:12:06Z
emits: []
framework: react
variant: ["default","sm","borderless","ghost"]
content_hash: 3d507a1d09f63f802915ee3d6f0ab13a86849d1e368fbfd04a5fa2d6ce0fdd63
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/card.tsx
signature:
  Card(props: { size?: 'default' | 'sm'; variant?: 'default' | 'borderless' | 'ghost'; className?: string; children: ReactNode }): JSX.Element

description:
  Contenedor card con header, title, description, action, content y footer. Sistema de slots composable. Variantes default, borderless y ghost para dashboards dark.

example:
  ```tsx
  <Card>
    <CardHeader>
      <CardTitle>Título</CardTitle>
      <CardDescription>Descripción</CardDescription>
    </CardHeader>
    <CardContent>Contenido</CardContent>
    <CardFooter>Footer</CardFooter>
  </Card>

  {/* Dashboard dark — sin bordes */}
  <Card variant="borderless">
    <CardContent>Widget sin marco</CardContent>
  </Card>

  {/* Completamente transparente */}
  <Card variant="ghost">
    <CardContent>Sin fondo ni borde</CardContent>
  </Card>
  ```

props:
  [{"name":"size","type":"'default' | 'sm'","required":false,"description":"Tamaño del card"},{"name":"variant","type":"'default' | 'borderless' | 'ghost'","required":false,"description":"Variante visual. borderless quita borde/shadow, ghost además hace bg transparente"},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]

notes:
  Sistema de slots via data-slot attributes. Card detecta automáticamente la presencia de CardFooter y ajusta el padding. Exporta 7 subcomponentes composables.

  Las variantes `borderless` y `ghost` eliminan el `ring-1` del borde por defecto. `ghost` además hace el fondo transparente. Alternativa con CSS global: `[data-slot="card"] { --tw-ring-opacity: 0; }` o `[data-variant="borderless"] { ring: 0 }` via `data-variant` attribute expuesto.

code:
  import * as React from 'react'
  import { Paper, Box, Text } from '@mantine/core'

  type CardVariant = 'default' | 'borderless' | 'ghost'

  function Card({
    className,
    size = 'default',
    variant = 'default',
    children,
    ...props
  }: React.ComponentProps<'div'> & { size?: 'default' | 'sm'; variant?: CardVariant }) {
    return (
      <Paper
        data-slot="card"
        data-size={size}
        data-variant={variant}
        withBorder={variant === 'default'}
        shadow={variant === 'default' ? 'xs' : undefined}
        radius="md"
        p={size === 'sm' ? 'sm' : 'md'}
        bg={variant === 'ghost' ? 'transparent' : undefined}
        className={className}
        {...props}
      >
        {children}
      </Paper>
    )
  }

  function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
    return (
      <Box
        data-slot="card-header"
        pb="xs"
        className={className}
        {...props}
      />
    )
  }

  function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
    return (
      <Text
        component="div"
        data-slot="card-title"
        fw={600}
        size="sm"
        className={className}
        {...props}
      />
    )
  }

  function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
    return (
      <Text
        component="div"
        data-slot="card-description"
        size="sm"
        c="dimmed"
        className={className}
        {...props}
      />
    )
  }

  function CardAction({ className, style, ...props }: React.ComponentProps<'div'>) {
    return (
      <Box
        data-slot="card-action"
        style={{ position: 'absolute', top: 0, right: 0, ...style }}
        className={className}
        {...props}
      />
    )
  }

  function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
    return (
      <Box
        data-slot="card-content"
        className={className}
        {...props}
      />
    )
  }

  function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
    return (
      <Box
        data-slot="card-footer"
        pt="sm"
        mt="auto"
        style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}
        className={className}
        {...props}
      />
    )
  }

  export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }

params_schema:
  {"output":"Componente Card que renderiza un contenedor con slots composables (header, content, footer) y 3 variantes visuales"}


id: chart_container_ts_ui
name: chart_container
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: ChartContainer(props: { children: ReactNode; height?: number | string }): JSX.Element
description: Thin wrapper Paper y utilidades de colores/series para los charts @mantine/charts.
tags: ["chart","container","mantine","base","visualization","component","ui"]
uses_functions: []
uses_types: ["ChartSeries_ts_ui"]
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/chart_container.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:11:54Z
emits: []
framework: react
variant: ["default"]
content_hash: 289ff310f93689519a41311225239b7463e0cf44c817278c0404792b991386dd
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/chart-base.tsx
example:
  ```tsx
  import { ChartContainer, getSeriesColor, type Series } from './chart_container'

  <ChartContainer height={400}>
    <MantineLineChart ... />
  </ChartContainer>
  ```

props:
  [{"name":"height","type":"number | string","required":false,"description":"Altura del chart (default 300)"},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]

notes:
  Exporta: ChartContainer, defaultColors, getSeriesColor, Series. Wrapper fino sobre Mantine Paper para layout uniforme de charts.

code:
  import { Paper } from '@mantine/core'

  export const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']

  export interface Series {
    key: string
    name: string
    color?: string
  }

  export function getSeriesColor(index: number, color?: string): string {
    return color || defaultColors[index % defaultColors.length]!
  }

  interface ChartContainerProps {
    children: React.ReactNode
    height?: number | string
  }

  export function ChartContainer({ children, height = 300 }: ChartContainerProps) {
    return (
      <Paper p="md" style={{ height, minWidth: 100, minHeight: typeof height === 'number' ? Math.min(height, 100) : 100 }}>
        {children}
      </Paper>
    )
  }

  /** @deprecated Mantine charts handle tooltips internally. Kept for index.ts compat. */
  export function ChartTooltipContent() { return null }
  /** @deprecated Mantine charts handle tooltips internally. Kept for index.ts compat. */
  export function ChartTooltip() { return null }
  /** @deprecated Mantine charts handle legends internally. Kept for index.ts compat. */
  export function ChartLegend() { return null }

params_schema:
  {"output":"Componente ChartContainer Paper wrapper y utilidades getSeriesColor/Series para charts Mantine"}


id: checkbox_ts_ui
name: checkbox
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Checkbox(props: CheckboxProps): JSX.Element
description: Input booleano accesible con label opcional y variante indeterminate. Mantine Checkbox.
tags: ["checkbox","component","ui","interactive","form","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/checkbox.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-06T00:00:55Z
emits: ["onCheckedChange"]
framework: react
variant: []
content_hash: 8196320b87dfd584dcb6195f14125fa6a6427001d058586f3e627e02c13c573f
example:
  ```tsx
  // Basico
  <Checkbox label="Acepto los terminos" />

  // Controlado
  <Checkbox
    label="Seleccionar todos"
    checked={allSelected}
    indeterminate={someSelected}
    onCheckedChange={setAllSelected}
  />

  // Sin label
  <Checkbox checked={isActive} onCheckedChange={setIsActive} />
  ```

props:
  [{"name":"label","type":"string","required":false,"description":"Texto de etiqueta visible junto al checkbox"},{"name":"indeterminate","type":"boolean","required":false,"description":"Estado indeterminate (guion) en vez de checked/unchecked"},{"name":"checked","type":"boolean","required":false,"description":"Estado controlado del checkbox"},{"name":"defaultChecked","type":"boolean","required":false,"description":"Estado inicial no controlado"},{"name":"disabled","type":"boolean","required":false,"description":"Deshabilita el checkbox"},{"name":"onCheckedChange","type":"(checked: boolean) =\u003e void","required":false,"description":"Callback cuando cambia el estado"}]

notes:
  Usa Mantine Checkbox para accesibilidad completa (keyboard, ARIA). El estado indeterminate se muestra con un guion horizontal. El callback onCheckedChange se adapta desde el onChange nativo de Mantine.

code:
  import { Checkbox as MantineCheckbox } from "@mantine/core"

  interface CheckboxProps {
    label?: string
    indeterminate?: boolean
    className?: string
    labelClassName?: string
    checked?: boolean
    defaultChecked?: boolean
    disabled?: boolean
    onCheckedChange?: (checked: boolean) => void
    id?: string
  }

  function Checkbox({ className, label, id, indeterminate, checked, defaultChecked, disabled, onCheckedChange, ...props }: CheckboxProps) {
    return (
      <MantineCheckbox
        id={id}
        data-slot="checkbox"
        label={label}
        indeterminate={indeterminate}
        checked={checked}
        defaultChecked={defaultChecked}
        disabled={disabled}
        onChange={(event) => onCheckedChange?.(event.currentTarget.checked)}
        className={className}
        size="sm"
        {...props}
      />
    )
  }

  export { Checkbox }
  export type { CheckboxProps }

params_schema:
  {"output":"Componente Checkbox que renderiza input booleano accesible con label opcional y estado indeterminate"}


id: chip_ts_ui
name: chip
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Chip(props: ChipProps): JSX.Element
tags: ["chip","toggle","selection","component","ui","mantine"]
uses_functions: []
uses_types: []
returns: []
imports: ["@mantine/core"]
tests: []
file_path: frontend/functions/ui/chip.tsx
created_at: 2026-04-06T10:42:46Z
updated_at: 2026-04-06T10:42:46Z
emits: ["onChange"]
framework: react
variant: ["filled","outline","light"]
content_hash: bb6ff52eda5c53b81f97c635e9e60816dceb3510f83d2f79596e840b1f2864a4
description:
  Chip seleccionable con variantes filled/outline/light. ChipGroup para selección simple o múltiple. Wrapper sobre Mantine Chip.

example:
  ```tsx
  import { Chip, ChipGroup } from '@fn_library'

  // Chip individual controlado
  <Chip checked={active} onChange={setActive}>
    Activo
  </Chip>

  // ChipGroup selección simple (una sola opción)
  <ChipGroup value={selected} onChange={setSelected}>
    <Chip value="react">React</Chip>
    <Chip value="vue">Vue</Chip>
    <Chip value="svelte">Svelte</Chip>
  </ChipGroup>

  // ChipGroup selección múltiple
  <ChipGroup multiple value={tags} onChange={setTags}>
    <Chip value="frontend">Frontend</Chip>
    <Chip value="backend">Backend</Chip>
    <Chip value="devops">DevOps</Chip>
  </ChipGroup>

  // Con variante y color custom
  <Chip variant="outline" color="teal" size="lg">
    Destacado
  </Chip>
  ```

props:
  [{"name":"checked","type":"boolean","required":false,"description":"Estado seleccionado del chip (controlled)"},{"name":"onChange","type":"(checked: boolean) =\u003e void","required":false,"description":"Callback al cambiar el estado del chip"},{"name":"variant","type":"'filled' | 'outline' | 'light'","required":false,"description":"Estilo visual del chip"},{"name":"color","type":"string","required":false,"description":"Color del chip cuando está seleccionado"},{"name":"size","type":"'xs' | 'sm' | 'md' | 'lg' | 'xl'","required":false,"description":"Tamaño del chip"},{"name":"children","type":"React.ReactNode","required":true,"description":"Contenido del chip — texto o elemento"}]

notes:
  - Wrapper directo sobre `Chip` de `@mantine/core` v9. Todas las props de Mantine Chip son válidas.
  - `ChipGroup` es un alias de `MantineChip.Group` — gestiona el estado de selección entre chips hijos.
  - En `ChipGroup` sin `multiple`, `value` es `string`. Con `multiple`, `value` es `string[]`.
  - Internamente cada `Chip` renderiza un checkbox/radio accesible oculto con label visual.

code:
  import { Chip as MantineChip, type ChipProps as MantineChipProps } from '@mantine/core'

  interface ChipProps extends MantineChipProps {}

  function Chip(props: ChipProps) {
    return <MantineChip {...props} />
  }

  const ChipGroup = MantineChip.Group

  export { Chip, ChipGroup }
  export type { ChipProps }

params_schema:
  {"output":"Componente Chip que renderiza un elemento seleccionable tipo badge, con ChipGroup para gestionar selección simple o múltiple entre varios chips"}


