# functions

id: cdp_click_go_browser
name: cdp_click
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpClick(c *CDPConn, selector string) error
tags: ["chrome","cdp","browser","automation","click","dom","devtools"]
uses_functions: ["cdp_connect_go_browser","cdp_evaluate_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","strconv","strings"]
tests: []
file_path: functions/browser/cdp_click.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 99755ef4969e2a165d16ed3f99d885b52e3fa20e7cba2f4fc65eaabf90e8b907
description:
  Hace click en el primer elemento que coincide con el selector CSS. Obtiene coordenadas del centro via getBoundingClientRect, hace scroll al elemento y despacha eventos mousedown+mouseup via Input.dispatchMouseEvent.

example:
  ```go
  conn, _ := CdpConnect(9222)
  CdpNavigate(conn, "https://example.com")

  // Click en el primer enlace
  if err := CdpClick(conn, "a"); err != nil {
      log.Fatal(err)
  }

  // Click en boton por ID
  if err := CdpClick(conn, "#submit-btn"); err != nil {
      log.Fatal(err)
  }
  ```

notes:
  El selector sigue la sintaxis CSS estandar (IDs, clases, atributos, pseudo-selectores). El elemento debe ser visible en el DOM en el momento del click. Si no se encuentra, retorna error inmediatamente sin esperar — combinar con `CdpWaitElement` para elementos dinamicos.

code:
  package browser

  import (
  	"fmt"
  	"strconv"
  	"strings"
  )

  // CdpClick hace click en el primer elemento que coincide con el selector CSS.
  // Obtiene las coordenadas del elemento via Runtime.evaluate y luego despacha
  // eventos mousedown, mouseup y click via Input.dispatchMouseEvent.
  func CdpClick(c *CDPConn, selector string) error {
  	if c == nil {
  		return fmt.Errorf("cdp click: conexion nula")
  	}

  	// Obtener coordenadas del centro del elemento
  	js := fmt.Sprintf(`(function() {
  		var el = document.querySelector(%q);
  		if (!el) return null;
  		var r = el.getBoundingClientRect();
  		return JSON.stringify({x: r.left + r.width/2, y: r.top + r.height/2});
  	})()`, selector)

  	coordStr, err := CdpEvaluate(c, js)
  	if err != nil {
  		return fmt.Errorf("cdp click: obtener coordenadas de %q: %w", selector, err)
  	}
  	if coordStr == "" || coordStr == "null" {
  		return fmt.Errorf("cdp click: elemento %q no encontrado en el DOM", selector)
  	}

  	// Parsear "{x:...,y:...}" — CdpEvaluate ya retorna el JSON como string
  	coordStr = strings.Trim(coordStr, `"`)
  	x, y, err := parseCoords(coordStr)
  	if err != nil {
  		return fmt.Errorf("cdp click: parsear coordenadas %q: %w", coordStr, err)
  	}

  	// Hacer scroll al elemento para que este visible
  	scrollJS := fmt.Sprintf(`document.querySelector(%q).scrollIntoView({block:'center'})`, selector)
  	if _, err := CdpEvaluate(c, scrollJS); err != nil {
  		// No es fatal si el scroll falla
  		_ = err
  	}

  	// Despachar mousedown
  	mouseParams := map[string]any{
  		"type":       "mousePressed",
  		"x":          x,
  		"y":          y,
  		"button":     "left",
  		"clickCount": 1,
  	}
  	if _, err := c.sendCDP("Input.dispatchMouseEvent", mouseParams); err != nil {
  		return fmt.Errorf("cdp click: mousedown: %w", err)
  	}

  	// Despachar mouseup
  	mouseParams["type"] = "mouseReleased"
  	if _, err := c.sendCDP("Input.dispatchMouseEvent", mouseParams); err != nil {
  		return fmt.Errorf("cdp click: mouseup: %w", err)
  	}

  	return nil
  }

  // parseCoords extrae x e y de un string JSON como {"x":100,"y":200}.
  func parseCoords(s string) (float64, float64, error) {
  	// Buscar valores x e y manualmente para evitar dependencia de encoding/json
  	s = strings.TrimSpace(s)
  	s = strings.TrimPrefix(s, "{")
  	s = strings.TrimSuffix(s, "}")

  	var x, y float64
  	for part := range strings.SplitSeq(s, ",") {
  		kv := strings.SplitN(strings.TrimSpace(part), ":", 2)
  		if len(kv) != 2 {
  			continue
  		}
  		k := strings.Trim(strings.TrimSpace(kv[0]), `"`)
  		v, err := strconv.ParseFloat(strings.TrimSpace(kv[1]), 64)
  		if err != nil {
  			return 0, 0, fmt.Errorf("parsear valor %q: %w", kv[1], err)
  		}
  		switch k {
  		case "x":
  			x = v
  		case "y":
  			y = v
  		}
  	}
  	return x, y, nil
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"},{"name":"selector","desc":"selector CSS del elemento a hacer click"}],"output":"error si el selector no coincide o falla la acción"}


id: cdp_close_go_browser
name: cdp_close
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpClose(c *CDPConn, pid int) error
tags: ["chrome","cdp","browser","automation","cleanup","devtools"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os"]
tests: []
file_path: functions/browser/cdp_close.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b3c28ba3893569d2217f41026174e230d246f6d4e2dfdc9f75000afa1c237768
description:
  Cierra la conexion WebSocket CDP y opcionalmente mata el proceso Chrome por PID. Si c es nil, solo mata el proceso. Si pid <= 0, solo cierra la conexion. Siempre intenta ambas operaciones aunque una falle.

example:
  ```go
  pid, _ := ChromeLaunch(ChromeLaunchOpts{Port: 9222, Headless: true})
  conn, _ := CdpConnect(9222)

  defer CdpClose(conn, pid)  // cierra WebSocket y mata Chrome

  // O por separado:
  defer CdpClose(conn, 0)  // solo cierra WebSocket
  defer CdpClose(nil, pid) // solo mata Chrome
  ```

notes:
  Usar en `defer` para garantizar cleanup. Si tanto la conexion como el proceso son invalidos, el error retornado corresponde al primero que fallo. Marca `c.closed = true` para evitar doble cierre.

code:
  package browser

  import (
  	"fmt"
  	"os"
  )

  // CdpClose cierra la conexion WebSocket CDP y, si pid > 0, mata el proceso Chrome.
  // Siempre intenta cerrar la conexion aunque el kill falle, y viceversa.
  // Retorna el primer error encontrado.
  func CdpClose(c *CDPConn, pid int) error {
  	var firstErr error

  	if c != nil && !c.closed {
  		c.closed = true
  		if err := c.conn.Close(); err != nil {
  			firstErr = fmt.Errorf("cdp close: cerrar websocket: %w", err)
  		}
  	}

  	if pid > 0 {
  		proc, err := os.FindProcess(pid)
  		if err != nil {
  			if firstErr == nil {
  				firstErr = fmt.Errorf("cdp close: encontrar proceso %d: %w", pid, err)
  			}
  		} else {
  			if err := proc.Kill(); err != nil {
  				if firstErr == nil {
  					firstErr = fmt.Errorf("cdp close: matar proceso %d: %w", pid, err)
  				}
  			}
  		}
  	}

  	return firstErr
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP (puede ser nil)"},{"name":"pid","desc":"PID del proceso Chrome (0 para no matar)"}],"output":"error si falla la desconexión o el cierre del proceso"}


id: cdp_connect_go_browser
name: cdp_connect
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpConnect(port int) (*CDPConn, error)
tags: ["chrome","cdp","browser","automation","websocket","devtools"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","net","net/url","strings"]
tests: ["TestChromeLaunchAndConnect"]
test_file_path: functions/browser/chrome_launch_test.go
file_path: functions/browser/cdp_connect.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 6049e1ed33eb40e04a1bca7f4d67b0831083f8b9ec17a03c4861645ea220e868
description:
  Se conecta al endpoint CDP en localhost:{port}. Obtiene el webSocketDebuggerUrl via HTTP /json/version, realiza el handshake WebSocket RFC 6455 sobre TCP puro (sin dependencias externas) y retorna una CDPConn lista para usar. Inicia goroutine de lectura de mensajes.

example:
  ```go
  conn, err := CdpConnect(9222)
  if err != nil {
      log.Fatal(err)
  }
  defer CdpClose(conn, 0)
  ```

notes:
  El WebSocket se implementa desde cero usando `net.Dial` + handshake HTTP Upgrade + framing RFC 6455. Esto evita dependencias externas. Solo soporta frames no fragmentados (suficiente para CDP que envia mensajes completos).

  El struct `CDPConn` y toda la infraestructura WebSocket/CDP se define en `cdp_conn.go` dentro del mismo paquete `infra`.

  La goroutine `readLoop` enruta respuestas a channels por ID de mensaje. Los eventos CDP sin ID son ignorados (las funciones que necesitan eventos usan polling).

code:
  package browser

  import (
  	"encoding/json"
  	"fmt"
  	"net"
  	"net/http"
  	"net/url"
  	"strings"
  )

  // cdpTarget representa un target CDP del endpoint /json.
  type cdpTarget struct {
  	ID                   string `json:"id"`
  	Type                 string `json:"type"`
  	Title                string `json:"title"`
  	URL                  string `json:"url"`
  	WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
  }

  // cdpGetPageWSURL obtiene el webSocketDebuggerUrl de la primera tab de tipo "page"
  // via el endpoint /json. Si no hay ninguna, crea una nueva con /json/new.
  func cdpGetPageWSURL(host string, port int) (string, error) {
  	if host == "" {
  		host = "localhost"
  	}
  	resp, err := http.Get(fmt.Sprintf("http://%s:%d/json", host, port))
  	if err != nil {
  		return "", fmt.Errorf("cdp targets: %w", err)
  	}
  	defer resp.Body.Close()

  	var targets []cdpTarget
  	if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil {
  		return "", fmt.Errorf("cdp targets: decode: %w", err)
  	}

  	// Buscar la primera tab de tipo "page"
  	for _, t := range targets {
  		if t.Type == "page" && t.WebSocketDebuggerURL != "" {
  			return t.WebSocketDebuggerURL, nil
  		}
  	}

  	// No hay tabs — crear una nueva via /json/new
  	newResp, err := http.Get(fmt.Sprintf("http://%s:%d/json/new", host, port))
  	if err != nil {
  		return "", fmt.Errorf("cdp new tab: %w", err)
  	}
  	defer newResp.Body.Close()

  	var newTarget cdpTarget
  	if err := json.NewDecoder(newResp.Body).Decode(&newTarget); err != nil {
  		return "", fmt.Errorf("cdp new tab: decode: %w", err)
  	}
  	if newTarget.WebSocketDebuggerURL == "" {
  		return "", fmt.Errorf("cdp new tab: webSocketDebuggerUrl vacio")
  	}
  	return newTarget.WebSocketDebuggerURL, nil
  }

  // CdpConnect se conecta al endpoint CDP en localhost:{port} y retorna una CDPConn lista para usar.
  // Conecta a la primera tab de tipo "page" (que soporta Page.*, Runtime.*, Input.*).
  // Si no hay tabs disponibles, crea una nueva via /json/new.
  // Realiza el handshake WebSocket RFC 6455 sobre TCP puro (sin dependencias externas).
  func CdpConnect(port int) (*CDPConn, error) {
  	return CdpConnectHost("localhost", port)
  }

  // CdpConnectHost es como CdpConnect pero permite especificar el host.
  // Util para WSL2 donde Chrome Windows escucha en una IP distinta a localhost.
  func CdpConnectHost(host string, port int) (*CDPConn, error) {
  	if host == "" {
  		host = "localhost"
  	}
  	wsURL, err := cdpGetPageWSURL(host, port)
  	if err != nil {
  		return nil, fmt.Errorf("cdp connect: %w", err)
  	}

  	// Parsear la URL del WebSocket para extraer host y path
  	u, err := url.Parse(wsURL)
  	if err != nil {
  		return nil, fmt.Errorf("cdp connect: parsear ws url %q: %w", wsURL, err)
  	}

  	wsHost := u.Host
  	if !strings.Contains(wsHost, ":") {
  		wsHost = wsHost + ":80"
  	}

  	// Abrir conexion TCP
  	tcpConn, err := net.Dial("tcp", wsHost)
  	if err != nil {
  		return nil, fmt.Errorf("cdp connect: tcp dial %s: %w", wsHost, err)
  	}

  	// Realizar handshake WebSocket
  	path := u.RequestURI()
  	reader, err := wsHandshake(tcpConn, wsHost, path)
  	if err != nil {
  		tcpConn.Close()
  		return nil, fmt.Errorf("cdp connect: ws handshake: %w", err)
  	}

  	c := &CDPConn{
  		conn:    tcpConn,
  		reader:  reader,
  		port:    port,
  		pending: make(map[int64]chan cdpResponse),
  	}

  	// Iniciar goroutine de lectura
  	go c.readLoop()

  	return c, nil
  }

params_schema:
  {"params":[{"name":"port","desc":"puerto del servidor CDP (localhost:{port})"}],"output":"(*CDPConn, error): conexión WebSocket lista para usar"}


id: cdp_evaluate_go_browser
name: cdp_evaluate
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpEvaluate(c *CDPConn, expression string) (string, error)
tags: ["chrome","cdp","browser","automation","javascript","devtools"]
uses_functions: ["cdp_connect_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: ["TestCdpEvaluate"]
test_file_path: functions/browser/chrome_launch_test.go
file_path: functions/browser/cdp_evaluate.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b83ac12c8f53a11846d90c5329c3e2c769be4ca86ee94429918ba4e84b64c365
description:
  Ejecuta una expresion JavaScript arbitraria en la pagina actual via Runtime.evaluate. Retorna el resultado serializado como string. Soporta await (awaitPromise=true). Reporta excepciones JS como error.

example:
  ```go
  conn, _ := CdpConnect(9222)
  result, err := CdpEvaluate(conn, "document.title")
  // result = "My Page Title"

  sum, err := CdpEvaluate(conn, "1 + 2")
  // sum = "3"
  ```

notes:
  Los valores no-string se convierten con `fmt.Sprintf("%v", value)`. Numeros aparecen sin decimales si son enteros (ej: `"3"` no `"3.0"`). Para tipos complejos (objetos, arrays), el resultado es la representacion Go de la interfaz, no JSON — usar `JSON.stringify(...)` en la expresion JS para obtener JSON limpio.

code:
  package browser

  import (
  	"fmt"
  )

  // CdpEvaluate ejecuta una expresion JavaScript en la pagina actual via Runtime.evaluate.
  // Retorna el resultado como string. Soporta expresiones simples y complejas.
  // Para valores no-string, Chrome los serializa a JSON.
  func CdpEvaluate(c *CDPConn, expression string) (string, error) {
  	if c == nil {
  		return "", fmt.Errorf("cdp evaluate: conexion nula")
  	}

  	params := map[string]any{
  		"expression":            expression,
  		"returnByValue":         true,
  		"awaitPromise":          true,
  		"userGesture":           true,
  	}

  	result, err := c.sendCDP("Runtime.evaluate", params)
  	if err != nil {
  		return "", fmt.Errorf("cdp evaluate: %w", err)
  	}

  	// Verificar excepcion JS
  	if exc, ok := result["exceptionDetails"]; ok && exc != nil {
  		excMap, _ := exc.(map[string]any)
  		text, _ := excMap["text"].(string)
  		return "", fmt.Errorf("cdp evaluate: excepcion JS: %s", text)
  	}

  	// Extraer valor del resultado
  	resVal, ok := result["result"].(map[string]any)
  	if !ok {
  		return "", fmt.Errorf("cdp evaluate: resultado inesperado: %v", result)
  	}

  	value, ok := resVal["value"]
  	if !ok {
  		// Puede ser undefined o un tipo no serializable
  		typ, _ := resVal["type"].(string)
  		return typ, nil
  	}

  	return fmt.Sprintf("%v", value), nil
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"},{"name":"expression","desc":"expresión JavaScript a evaluar"}],"output":"string: resultado serializado, o error si falla"}


id: cdp_get_html_go_browser
name: cdp_get_html
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpGetHTML(c *CDPConn) (string, error)
tags: ["chrome","cdp","browser","automation","html","dom","scraping","devtools"]
uses_functions: ["cdp_connect_go_browser","cdp_evaluate_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: ["TestCdpGetHTML"]
test_file_path: functions/browser/chrome_launch_test.go
file_path: functions/browser/cdp_get_html.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c5f542cedb9eb7ec12bca007f3fe6e50a17ab7dee61d6841f42ec44932b0ac34
description:
  Retorna el HTML completo de la pagina actual (document.documentElement.outerHTML) via Runtime.evaluate. Captura el DOM vivo post-JavaScript, no el HTML fuente original.

example:
  ```go
  conn, _ := CdpConnect(9222)
  CdpNavigate(conn, "https://example.com")
  CdpWaitElement(conn, "body", 5*time.Second)

  html, err := CdpGetHTML(conn)
  // html contiene el DOM completo con todos los cambios JS aplicados
  ```

notes:
  A diferencia de `Page.getResourceContent`, esta funcion captura el estado actual del DOM incluyendo modificaciones hechas por JavaScript. Ideal para scraping de SPAs (React, Vue, Angular). El HTML retornado puede ser muy largo para paginas complejas.

code:
  package browser

  import (
  	"fmt"
  )

  // CdpGetHTML retorna el HTML completo de la pagina actual (document.documentElement.outerHTML).
  // Equivale a hacer "Ver codigo fuente" pero con el DOM actual (post-JavaScript).
  func CdpGetHTML(c *CDPConn) (string, error) {
  	if c == nil {
  		return "", fmt.Errorf("cdp get html: conexion nula")
  	}

  	html, err := CdpEvaluate(c, "document.documentElement.outerHTML")
  	if err != nil {
  		return "", fmt.Errorf("cdp get html: %w", err)
  	}

  	return html, nil
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"}],"output":"string: HTML completo de la página (post-JavaScript)"}


id: cdp_navigate_go_browser
name: cdp_navigate
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpNavigate(c *CDPConn, targetURL string) error
tags: ["chrome","cdp","browser","automation","navigation","devtools"]
uses_functions: ["cdp_connect_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: ["TestChromeLaunchAndConnect"]
test_file_path: functions/browser/chrome_launch_test.go
file_path: functions/browser/cdp_navigate.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a6231592bf2eb2c6d8f65e3fde65d60632e6c1e8a75d104d387f888269d5cc3b
description:
  Navega a la URL indicada usando el comando Page.navigate del protocolo CDP. Verifica que no haya errorText en la respuesta. Recibe una *CDPConn obtenida de CdpConnect.

example:
  ```go
  conn, _ := CdpConnect(9222)
  defer CdpClose(conn, 0)

  if err := CdpNavigate(conn, "https://example.com"); err != nil {
      log.Fatal(err)
  }
  ```

notes:
  Usa `Page.navigate` que es sincrono en la respuesta CDP pero la carga completa de la pagina puede tardar mas. Para esperar elementos especificos tras la navegacion, usar `CdpWaitElement`.

code:
  package browser

  import (
  	"fmt"
  )

  // CdpNavigate navega a la URL indicada usando Page.navigate.
  // Espera a que la carga este confirmada via Page.loadEventFired antes de retornar.
  // El timeout de la navegacion es gestionado por Chrome internamente.
  func CdpNavigate(c *CDPConn, targetURL string) error {
  	if c == nil {
  		return fmt.Errorf("cdp navigate: conexion nula")
  	}

  	params := map[string]any{
  		"url": targetURL,
  	}

  	result, err := c.sendCDP("Page.navigate", params)
  	if err != nil {
  		return fmt.Errorf("cdp navigate: %w", err)
  	}

  	// Verificar que no hubo error de navegacion
  	if errText, ok := result["errorText"].(string); ok && errText != "" {
  		return fmt.Errorf("cdp navigate: error navegando a %q: %s", targetURL, errText)
  	}

  	return nil
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"},{"name":"targetURL","desc":"URL a navegar"}],"output":"error si la navegación falla"}


id: cdp_screenshot_go_browser
name: cdp_screenshot
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpScreenshot(c *CDPConn, outputPath string, opts CdpScreenshotOpts) error
tags: ["chrome","cdp","browser","automation","screenshot","devtools","png"]
uses_functions: ["cdp_connect_go_browser","cdp_evaluate_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["encoding/base64","fmt","os","path/filepath"]
tests: ["TestCdpScreenshot"]
test_file_path: functions/browser/chrome_launch_test.go
file_path: functions/browser/cdp_screenshot.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: dd55c0907fadff3b743e06487dee583e691f2997b6bff48ab53944309a79df93
description:
  Captura un screenshot de la pagina actual via Page.captureScreenshot y lo guarda en el archivo indicado. Soporta PNG y JPEG, viewport o pagina completa. Crea el directorio destino si no existe.

example:
  ```go
  conn, _ := CdpConnect(9222)
  CdpNavigate(conn, "https://example.com")

  err := CdpScreenshot(conn, "/tmp/page.png", CdpScreenshotOpts{
      FullPage: true,
      Format:   "png",
  })
  ```

notes:
  El struct `CdpScreenshotOpts` tiene campos: `FullPage bool`, `Quality int` (JPEG), `Format string` ("png" o "jpeg"). Chrome retorna la imagen como base64 que se decodifica y escribe al disco.

code:
  package browser

  import (
  	"encoding/base64"
  	"fmt"
  	"os"
  	"path/filepath"
  )

  // CdpScreenshotOpts configura el screenshot.
  type CdpScreenshotOpts struct {
  	// FullPage indica si capturar la pagina completa (scroll height) o solo el viewport.
  	FullPage bool
  	// Quality es la calidad JPEG (0-100). Solo aplica si Format es "jpeg". Por defecto 80.
  	Quality int
  	// Format es el formato de imagen: "png" o "jpeg". Por defecto "png".
  	Format string
  }

  // CdpScreenshot captura un screenshot de la pagina actual y lo guarda en outputPath.
  // Usa Page.captureScreenshot del protocolo CDP.
  // outputPath debe tener extension .png o .jpg/.jpeg segun el formato elegido.
  func CdpScreenshot(c *CDPConn, outputPath string, opts CdpScreenshotOpts) error {
  	if c == nil {
  		return fmt.Errorf("cdp screenshot: conexion nula")
  	}

  	if opts.Format == "" {
  		opts.Format = "png"
  	}
  	if opts.Quality == 0 && opts.Format == "jpeg" {
  		opts.Quality = 80
  	}

  	params := map[string]any{
  		"format":  opts.Format,
  		"captureBeyondViewport": opts.FullPage,
  	}
  	if opts.Format == "jpeg" {
  		params["quality"] = opts.Quality
  	}

  	if opts.FullPage {
  		// Expandir clip para capturar toda la pagina
  		scrollHeight, err := CdpEvaluate(c, "document.documentElement.scrollHeight")
  		if err == nil {
  			params["clip"] = nil // dejar que Chrome capture todo
  			_ = scrollHeight
  		}
  	}

  	result, err := c.sendCDP("Page.captureScreenshot", params)
  	if err != nil {
  		return fmt.Errorf("cdp screenshot: %w", err)
  	}

  	dataStr, ok := result["data"].(string)
  	if !ok {
  		return fmt.Errorf("cdp screenshot: campo data ausente en respuesta")
  	}

  	imgData, err := base64.StdEncoding.DecodeString(dataStr)
  	if err != nil {
  		return fmt.Errorf("cdp screenshot: decodificar base64: %w", err)
  	}

  	// Crear directorio si no existe
  	if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
  		return fmt.Errorf("cdp screenshot: crear directorio: %w", err)
  	}

  	if err := os.WriteFile(outputPath, imgData, 0644); err != nil {
  		return fmt.Errorf("cdp screenshot: guardar archivo: %w", err)
  	}

  	return nil
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"},{"name":"outputPath","desc":"ruta del archivo PNG/JPEG de salida"},{"name":"opts","desc":"opciones de captura (FullPage, Quality, Format)"}],"output":"error si falla la captura o la escritura del archivo"}


id: cdp_type_text_go_browser
name: cdp_type_text
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpTypeText(c *CDPConn, text string) error
tags: ["chrome","cdp","browser","automation","keyboard","input","devtools"]
uses_functions: ["cdp_connect_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","time"]
tests: []
file_path: functions/browser/cdp_type_text.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 6576a703e437c519fa5621a80f3ce50ca6b5752146e75c831dad0353d206e5cb
description:
  Escribe texto en el elemento activo de la pagina caracter por caracter via Input.dispatchKeyEvent. Envia eventos keyDown, char y keyUp por cada caracter con 10ms de pausa entre ellos. Usar CdpClick primero para enfocar el elemento.

example:
  ```go
  conn, _ := CdpConnect(9222)
  CdpNavigate(conn, "https://example.com/search")

  // Enfocar el campo de busqueda y escribir
  CdpClick(conn, "input[type=search]")
  CdpTypeText(conn, "golang websocket")
  ```

notes:
  Envia tres eventos por caracter: `keyDown`, `char` (dispara el evento `input` del DOM) y `keyUp`. La pausa de 10ms entre caracteres simula escritura humana y ayuda con inputs que tienen debounce. Para texto largo, considerar inyectar directamente via `CdpEvaluate` con `element.value = "..."` + evento `input`.

code:
  package browser

  import (
  	"fmt"
  	"time"
  )

  // CdpTypeText escribe texto en el elemento activo de la pagina caracter por caracter.
  // Usa Input.dispatchKeyEvent para simular pulsaciones de teclado reales.
  // Recomienda usar CdpClick primero para enfocar el elemento objetivo.
  func CdpTypeText(c *CDPConn, text string) error {
  	if c == nil {
  		return fmt.Errorf("cdp type text: conexion nula")
  	}

  	for _, ch := range text {
  		charStr := string(ch)

  		// keyDown
  		keyDown := map[string]any{
  			"type": "keyDown",
  			"key":  charStr,
  			"text": charStr,
  		}
  		if _, err := c.sendCDP("Input.dispatchKeyEvent", keyDown); err != nil {
  			return fmt.Errorf("cdp type text: keyDown %q: %w", charStr, err)
  		}

  		// char (dispara el evento input en el elemento)
  		keyChar := map[string]any{
  			"type": "char",
  			"key":  charStr,
  			"text": charStr,
  		}
  		if _, err := c.sendCDP("Input.dispatchKeyEvent", keyChar); err != nil {
  			return fmt.Errorf("cdp type text: char %q: %w", charStr, err)
  		}

  		// keyUp
  		keyUp := map[string]any{
  			"type": "keyUp",
  			"key":  charStr,
  			"text": charStr,
  		}
  		if _, err := c.sendCDP("Input.dispatchKeyEvent", keyUp); err != nil {
  			return fmt.Errorf("cdp type text: keyUp %q: %w", charStr, err)
  		}

  		// Pequena pausa entre caracteres para simular escritura humana
  		time.Sleep(10 * time.Millisecond)
  	}

  	return nil
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"},{"name":"text","desc":"texto a escribir en el elemento enfocado"}],"output":"error si falla la escritura"}


id: cdp_wait_element_go_browser
name: cdp_wait_element
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpWaitElement(c *CDPConn, selector string, timeout time.Duration) error
tags: ["chrome","cdp","browser","automation","dom","wait","polling","devtools"]
uses_functions: ["cdp_connect_go_browser","cdp_evaluate_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","time"]
tests: []
file_path: functions/browser/cdp_wait_element.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 536335ca430d574f868815c5094e09888afe55b2b7150b4540019b39d0af862c
description:
  Espera hasta que un selector CSS exista en el DOM. Hace polling con Runtime.evaluate cada 200ms. Retorna nil cuando el elemento aparece o error si se agota el timeout. Util despues de navegacion o acciones que producen cambios dinamicos.

example:
  ```go
  conn, _ := CdpConnect(9222)
  CdpNavigate(conn, "https://example.com")

  // Esperar hasta 10 segundos a que aparezca el contenido
  if err := CdpWaitElement(conn, ".main-content", 10*time.Second); err != nil {
      log.Fatal("Timeout esperando elemento:", err)
  }
  ```

notes:
  Usa `document.querySelector(selector) !== null` como condicion. Si `timeout <= 0` usa 10s por defecto. Los errores de `CdpEvaluate` durante el polling (pagina cargando) se ignoran y se reintenta en el siguiente ciclo.

code:
  package browser

  import (
  	"fmt"
  	"time"
  )

  // CdpWaitElement espera hasta que un selector CSS exista en el DOM.
  // Hace polling con Runtime.evaluate hasta que el elemento aparezca o se agote el timeout.
  // Retorna error si el timeout se agota sin encontrar el elemento.
  func CdpWaitElement(c *CDPConn, selector string, timeout time.Duration) error {
  	if c == nil {
  		return fmt.Errorf("cdp wait element: conexion nula")
  	}
  	if timeout <= 0 {
  		timeout = 10 * time.Second
  	}

  	deadline := time.Now().Add(timeout)
  	interval := 200 * time.Millisecond

  	js := fmt.Sprintf(`document.querySelector(%q) !== null`, selector)

  	for time.Now().Before(deadline) {
  		result, err := CdpEvaluate(c, js)
  		if err != nil {
  			// Error en evaluate — puede que la pagina aun no este lista
  			time.Sleep(interval)
  			continue
  		}
  		if result == "true" {
  			return nil
  		}
  		time.Sleep(interval)
  	}

  	return fmt.Errorf("cdp wait element: selector %q no encontrado despues de %s", selector, timeout)
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"},{"name":"selector","desc":"selector CSS del elemento a esperar"},{"name":"timeout","desc":"duración máxima de espera"}],"output":"error si el elemento no aparece antes del timeout"}


id: cdp_wait_load_go_browser
name: cdp_wait_load
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func CdpWaitLoad(c *CDPConn, timeout time.Duration) error
tags: ["chrome","cdp","browser","automation","wait","polling","devtools","readystate","load"]
uses_functions: ["cdp_evaluate_go_browser"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","time"]
tests: []
file_path: functions/browser/cdp_wait_load.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: e835fb63ec676cc2549712c84fc836a3b1d4efb701e5c941b62e1f24d254edda
description:
  Espera a que la pagina actual termine de cargar completamente. Hace polling de document.readyState via Runtime.evaluate cada 200ms hasta que sea "complete", o hasta que se agote el timeout. Retorna error inmediato si CdpEvaluate falla (la conexion puede estar rota).

example:
  ```go
  conn, _ := CdpConnect(9222)
  CdpNavigate(conn, "https://example.com")

  // Esperar hasta 30 segundos a que la pagina cargue por completo
  if err := CdpWaitLoad(conn, 30*time.Second); err != nil {
      log.Fatal("Timeout esperando carga:", err)
  }

  html, _ := CdpGetHTML(conn)
  ```

notes:
  A diferencia de `CdpWaitElement`, que ignora errores de `CdpEvaluate` durante el polling (la pagina puede aun no estar lista), `CdpWaitLoad` retorna el error inmediatamente porque un fallo en `document.readyState` indica una conexion rota, no una condicion transitoria.

  Si `timeout <= 0` usa 30s por defecto (mas largo que `CdpWaitElement` porque la carga completa de red puede tardar mas que la aparicion de un elemento DOM).

code:
  package browser

  import (
  	"fmt"
  	"time"
  )

  // CdpWaitLoad espera a que la página actual termine de cargar completamente.
  // Hace polling de document.readyState via Runtime.evaluate cada 200ms hasta
  // que sea "complete", o hasta que se agote el timeout.
  // Retorna error si el timeout se agota o si CdpEvaluate falla (conexion rota).
  func CdpWaitLoad(c *CDPConn, timeout time.Duration) error {
  	if c == nil {
  		return fmt.Errorf("cdp wait load: conexion nula")
  	}
  	if timeout <= 0 {
  		timeout = 30 * time.Second
  	}

  	deadline := time.Now().Add(timeout)
  	interval := 200 * time.Millisecond

  	for time.Now().Before(deadline) {
  		result, err := CdpEvaluate(c, "document.readyState")
  		if err != nil {
  			return fmt.Errorf("cdp wait load: error evaluando readyState: %w", err)
  		}
  		if result == "complete" {
  			return nil
  		}
  		time.Sleep(interval)
  	}

  	return fmt.Errorf("cdp wait load: pagina no cargo despues de %s", timeout)
  }

params_schema:
  {"params":[{"name":"c","desc":"conexión CDP activa"},{"name":"timeout","desc":"duración máxima de espera por carga completa"}],"output":"error si document.readyState no llega a complete"}


id: chrome_launch_go_browser
name: chrome_launch
kind: function
lang: go
domain: browser
version: 1.0.0
purity: impure
signature: func ChromeLaunch(opts ChromeLaunchOpts) (int, error)
tags: ["chrome","cdp","browser","automation","wsl2","headless"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","net","os","os/exec","time"]
tests: ["TestFindChrome","TestChromeLaunchAndConnect"]
test_file_path: functions/browser/chrome_launch_test.go
file_path: functions/browser/chrome_launch.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3948eaa2a88e4b386398cae2ec16f3d3ac380764e5333b10212e2f1b68e516ff
description:
  Lanza Google Chrome con remote debugging habilitado en el puerto indicado. Busca chrome.exe en PATH (WSL2) o en rutas conocidas de Windows. Espera hasta 15s a que el puerto CDP este listo antes de retornar. Retorna el PID del proceso.

example:
  ```go
  pid, err := ChromeLaunch(ChromeLaunchOpts{
      Port:        9222,
      UserDataDir: "/tmp/chrome-cdp",
      Headless:    true,
  })
  if err != nil {
      log.Fatal(err)
  }
  defer CdpClose(nil, pid)
  ```

notes:
  Busca Chrome en este orden:
  1. `chrome.exe` en PATH (disponible en WSL2 si Windows lo tiene en PATH)
  2. `google-chrome` / `chromium-browser` / `chromium` (Linux nativo)
  3. `/mnt/c/Program Files/Google/Chrome/Application/chrome.exe`
  4. `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe`

  Los flags aplicados desactivan funcionalidades de red y actualizacion en segundo plano para entornos de automatizacion. En modo headless se agrega `--headless=new --disable-gpu`.

  El struct `ChromeLaunchOpts` se define en el mismo archivo.

code:
  package browser

  import (
  	"fmt"
  	"net"
  	"os"
  	"os/exec"
  	"time"
  )

  // ChromeLaunchOpts configura el lanzamiento de Chrome con CDP.
  type ChromeLaunchOpts struct {
  	// Port es el puerto de remote debugging. Por defecto 9222.
  	Port int
  	// UserDataDir es el directorio de perfil de Chrome. Por defecto /tmp/chrome-cdp-profile.
  	UserDataDir string
  	// Headless activa el modo headless (--headless=new). Por defecto false.
  	Headless bool
  	// ChromePath es la ruta al ejecutable de Chrome. Si esta vacio, se busca automaticamente.
  	ChromePath string
  	// ExtraArgs permite pasar flags adicionales a Chrome.
  	ExtraArgs []string
  }

  // chromePaths lista los ejecutables de Chrome conocidos en WSL2/Linux.
  var chromePaths = []string{
  	"chrome.exe",
  	"google-chrome",
  	"chromium-browser",
  	"chromium",
  	"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
  	"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
  	"/mnt/c/Users/Public/Desktop/chrome.exe",
  }

  // findChrome localiza el ejecutable de Chrome en el sistema.
  func findChrome() (string, error) {
  	for _, p := range chromePaths {
  		if path, err := exec.LookPath(p); err == nil {
  			return path, nil
  		}
  		if _, err := os.Stat(p); err == nil {
  			return p, nil
  		}
  	}
  	return "", fmt.Errorf("chrome: ejecutable no encontrado en PATH ni en rutas conocidas de Windows")
  }

  // waitCDPReady espera hasta que el puerto CDP responda conexiones TCP.
  // host puede estar vacio (usa "localhost").
  func waitCDPReady(host string, port int, timeout time.Duration) error {
  	if host == "" {
  		host = "localhost"
  	}
  	deadline := time.Now().Add(timeout)
  	addr := fmt.Sprintf("%s:%d", host, port)
  	for time.Now().Before(deadline) {
  		conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
  		if err == nil {
  			conn.Close()
  			return nil
  		}
  		time.Sleep(200 * time.Millisecond)
  	}
  	return fmt.Errorf("chrome: puerto CDP %s:%d no disponible despues de %s", host, port, timeout)
  }

  // ChromeLaunch lanza Google Chrome con remote debugging habilitado en el puerto indicado.
  // Retorna el PID del proceso Chrome. Espera hasta 15s a que el puerto CDP este listo.
  // Busca chrome.exe en PATH (WSL2) o en rutas conocidas de Windows.
  func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
  	if opts.Port == 0 {
  		opts.Port = 9222
  	}
  	if opts.UserDataDir == "" {
  		opts.UserDataDir = "/tmp/chrome-cdp-profile"
  	}

  	chromePath := opts.ChromePath
  	if chromePath == "" {
  		var err error
  		chromePath, err = findChrome()
  		if err != nil {
  			return 0, err
  		}
  	}

  	args := []string{
  		fmt.Sprintf("--remote-debugging-port=%d", opts.Port),
  		fmt.Sprintf("--user-data-dir=%s", opts.UserDataDir),
  		"--no-first-run",
  		"--no-default-browser-check",
  		"--disable-background-networking",
  		"--disable-client-side-phishing-detection",
  		"--disable-default-apps",
  		"--disable-extensions",
  		"--disable-hang-monitor",
  		"--disable-popup-blocking",
  		"--disable-prompt-on-repost",
  		"--disable-sync",
  		"--disable-translate",
  		"--metrics-recording-only",
  		"--safebrowsing-disable-auto-update",
  	}

  	if opts.Headless {
  		args = append(args, "--headless=new", "--disable-gpu")
  	}

  	args = append(args, opts.ExtraArgs...)

  	cmd := exec.Command(chromePath, args...)
  	// Chrome necesita que no haya stderr/stdout bloqueados
  	cmd.Stdout = nil
  	cmd.Stderr = nil

  	if err := cmd.Start(); err != nil {
  		return 0, fmt.Errorf("chrome: arrancar proceso: %w", err)
  	}

  	pid := cmd.Process.Pid

  	// Esperar a que el puerto CDP este listo
  	// Si Chrome escucha en 0.0.0.0 (ej: WSL2 -> Windows), el caller se encarga del wait
  	skipWait := false
  	for _, a := range opts.ExtraArgs {
  		if a == "--remote-debugging-address=0.0.0.0" {
  			skipWait = true
  			break
  		}
  	}
  	if !skipWait {
  		if err := waitCDPReady("localhost", opts.Port, 15*time.Second); err != nil {
  			cmd.Process.Kill()
  			return 0, err
  		}
  	}

  	return pid, nil
  }

params_schema:
  {"params":[{"name":"opts","desc":"opciones de lanzamiento: Port, UserDataDir, Headless"}],"output":"int: PID del proceso Chrome lanzado"}


id: all_slice_go_core
name: all_slice
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func All[T any](slice []T, predicate func(T) bool) bool
description: Devuelve true si todos los elementos del slice cumplen el predicado.
tags: ["slice","functional","generic","predicate"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  result := All([]int{2, 4, 6}, func(x int) bool { return x%2 == 0 })
  // result = true
  ```

tests: []
file_path: functions/core/all_slice.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: b92405dd50841150ed7c872ef03d4ff8b3664b6afa39b5f74eb634228063d339
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // All devuelve true si todos los elementos del slice cumplen el predicado.
  func All[T any](slice []T, predicate func(T) bool) bool {
  	// stub
  	return false
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice de elementos a validar"},{"name":"predicate","desc":"función que evalúa cada elemento; retorna true si cumple la condición"}],"output":"true si todos los elementos cumplen el predicado; false en caso contrario"}


id: any_slice_go_core
name: any_slice
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Any[T any](slice []T, predicate func(T) bool) bool
description: Devuelve true si al menos un elemento del slice cumple el predicado.
tags: ["slice","functional","generic","predicate"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  result := Any([]int{1, 2, 3}, func(x int) bool { return x > 2 })
  // result = true
  ```

tests: []
file_path: functions/core/any_slice.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 4f394b63f2386bf07769badcb95b25cd036f275f20c3d90b36b5b6e0d619978a
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Any devuelve true si al menos un elemento del slice cumple el predicado.
  func Any[T any](slice []T, predicate func(T) bool) bool {
  	// stub
  	return false
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice de elementos a validar"},{"name":"predicate","desc":"función que evalúa cada elemento; retorna true si cumple la condición"}],"output":"true si al menos un elemento cumple el predicado; false en caso contrario"}


id: chunk_go_core
name: chunk
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Chunk[T any](xs []T, size int) [][]T
tags: ["slice","chunk","batch","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/chunk.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5bd28701026159cf5015e1931b1455900f1e9d8be73ecb6dbca334dc1bc6635f
description:
  Divide un slice en trozos (sub-slices) de tamanio N. El ultimo trozo puede contener menos de N elementos. Retorna nil si el slice esta vacio. Entra en panic si size <= 0.

example:
  ```go
  chunks := Chunk([]int{1, 2, 3, 4, 5}, 2)
  // chunks = [[1, 2], [3, 4], [5]]

  chunks = Chunk([]string{"a", "b", "c", "d"}, 4)
  // chunks = [["a", "b", "c", "d"]]
  ```

notes:
  Funcion pura generica. Los sub-slices comparten memoria con el slice original (son slices del mismo backing array). Pre-calcula la capacidad del slice de chunks para evitar reasignaciones. Si size es mayor que len(xs), retorna un unico chunk con todos los elementos.

code:
  package core

  // Chunk splits xs into sub-slices of the given size.
  // The last chunk may contain fewer than size elements.
  // Returns nil if xs is empty. Panics if size <= 0.
  func Chunk[T any](xs []T, size int) [][]T {
  	if size <= 0 {
  		panic("chunk size must be > 0")
  	}
  	n := len(xs)
  	if n == 0 {
  		return nil
  	}

  	numChunks := (n + size - 1) / size
  	chunks := make([][]T, 0, numChunks)
  	for i := 0; i < n; i += size {
  		end := i + size
  		if end > n {
  			end = n
  		}
  		chunks = append(chunks, xs[i:end])
  	}
  	return chunks
  }

params_schema:
  {"params":[{"name":"xs","desc":"slice de elementos a dividir"},{"name":"size","desc":"tamaño de cada trozo; debe ser \u003e 0 (entra en pánico si size \u003c= 0)"}],"output":"slice de slices, donde cada sub-slice contiene hasta 'size' elementos; el último puede contener menos"}


id: compare_versions_go_core
name: compare_versions
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func CompareVersions(a, b string) int
tags: ["core","version","semver","compare","parse"]
uses_functions: []
uses_types: []
returns: []
imports: ["strconv","strings"]
tests: []
file_path: functions/core/compare_versions.go
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 032febe0c81578ecf0b20a17f79078952cdf4fdd23d2a3acd6929b1637a32930
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: installer/core/version.go
description:
  Compara dos strings de version semantica (semver). Soporta formato con o sin prefijo 'v'. Retorna -1, 0 o 1.

example:
  ```go
  core.CompareVersions("v1.4.0", "v1.3.9")  // 1
  core.CompareVersions("1.0.0", "1.0.0")    // 0
  core.CompareVersions("v0.9.0", "v1.0.0")  // -1
  ```

notes:
  Compara major.minor.patch como enteros. Ignora pre-release tags y metadata (solo los 3 numeros). Partes faltantes se tratan como 0.

code:
  package core

  import (
  	"strconv"
  	"strings"
  )

  // CompareVersions compares two semver strings (e.g. "v1.4.0", "1.2.3").
  // Returns -1 if a < b, 0 if equal, 1 if a > b.
  func CompareVersions(a, b string) int {
  	pa := parseVersionParts(a)
  	pb := parseVersionParts(b)
  	for i := 0; i < 3; i++ {
  		if pa[i] < pb[i] {
  			return -1
  		}
  		if pa[i] > pb[i] {
  			return 1
  		}
  	}
  	return 0
  }

  func parseVersionParts(v string) [3]int {
  	v = strings.TrimPrefix(v, "v")
  	parts := strings.SplitN(v, ".", 3)
  	var nums [3]int
  	for i, p := range parts {
  		if i >= 3 {
  			break
  		}
  		n, _ := strconv.Atoi(p)
  		nums[i] = n
  	}
  	return nums
  }

params_schema:
  {"params":[{"name":"a","desc":"primera version (ej: 'v1.4.0' o '1.4.0')"},{"name":"b","desc":"segunda version"}],"output":"-1 si a \u003c b, 0 si iguales, 1 si a \u003e b"}


id: compose2_go_core
name: compose2
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Compose2[A, B, C any](f2 func(B) C, f1 func(A) B) func(A) C
description: Compone dos funciones de derecha a izquierda. compose2(g, f)(x) = g(f(x)).
tags: ["functional","composition","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/compose2.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: b98f81c72b263d9e17bb2356c440049d2d62c72196e66bfc8c6ddf5524dd6ad0
example:
  ```go
  double := func(x int) int { return x * 2 }
  toString := func(x int) string { return fmt.Sprintf("%d", x) }
  doubleStr := Compose2(toString, double)
  result := doubleStr(5) // "10"
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Compose2 compone dos funciones de derecha a izquierda.
  func Compose2[A, B, C any](f2 func(B) C, f1 func(A) B) func(A) C {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"f2","desc":"segunda función a aplicar (derecha): B -\u003e C"},{"name":"f1","desc":"primera función a aplicar (izquierda): A -\u003e B"}],"output":"función compuesta que aplica f1 después f2, equivalente a f2(f1(x))"}


id: const_func_go_core
name: const_func
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Const[T, U any](value T) func(U) T
description: Devuelve una funcion que siempre retorna el valor dado, ignorando su argumento.
tags: ["functional","generic","const"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  always42 := Const[int, string](42)
  result := always42("ignored") // 42
  ```

tests: []
file_path: functions/core/const_func.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 33db4007e2d469a3571e00fc5dbd7747e2b850071012814dd0a32d55a8963687
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Const devuelve una funcion que siempre retorna el valor dado, ignorando su argumento.
  func Const[T, U any](value T) func(U) T {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"value","desc":"valor constante a retornar siempre"}],"output":"función que ignora su argumento y siempre retorna el valor constante"}


id: cron_match_go_core
name: cron_match
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func CronMatch(sched CronSchedule, t time.Time) bool
tags: ["cron","scheduling","matching","time","pure"]
uses_functions: []
uses_types: ["cron_schedule_go_core"]
returns: []
imports: ["time"]
test_file_path: functions/core/cron_match_test.go
file_path: functions/core/cron_match.go
created_at: 2026-04-12T11:03:11Z
updated_at: 2026-04-12T11:03:11Z
props: []
emits: []
variant: []
content_hash: c0487732c3658a88757886e0e1d5a0859c0781f7f6dbec226a1891776165cb26
description:
  Verifica si un instante de tiempo coincide con un cron schedule. Compara los 5 campos (minuto, hora, dia del mes, mes, dia de la semana) y retorna true si todos coinciden.

example:
  ```go
  sched, _ := ParseCronExpr("0 9 * * *")
  t := time.Date(2026, 4, 11, 9, 0, 0, 0, time.UTC)
  CronMatch(sched, t) // true

  t2 := time.Date(2026, 4, 11, 10, 0, 0, 0, time.UTC)
  CronMatch(sched, t2) // false
  ```

tests:
  ["9:00 AM coincide con 0 9 * * *","9:15 AM NO coincide con 0 9 * * *","lunes a las 9 coincide con 0 9 * * 1","domingo a las 9 NO coincide con 0 9 * * 1","wildcard * coincide con cualquier valor","specific month"]

notes:
  Funcion pura. Usa AND semantics para day_of_month y day_of_week (ambos deben coincidir), igual que NextCronTime en el mismo paquete.
  Reutiliza el helper intIn definido en next_cron_time.go (mismo paquete core).

code:
  package core

  import "time"

  // CronMatch returns true if time t matches all fields of the cron schedule.
  func CronMatch(sched CronSchedule, t time.Time) bool {
  	return intIn(t.Minute(), sched.Minute) &&
  		intIn(t.Hour(), sched.Hour) &&
  		intIn(t.Day(), sched.DayOfMonth) &&
  		intIn(int(t.Month()), sched.Month) &&
  		intIn(int(t.Weekday()), sched.DayOfWeek)
  }

params_schema:
  {"params":[{"name":"sched","desc":"CronSchedule con listas de valores validos por campo (resultado de ParseCronExpr)"},{"name":"t","desc":"instante de tiempo a verificar contra el schedule"}],"output":"true si t coincide con todos los campos del cron schedule"}


id: curry2_go_core
name: curry2
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Curry2[A, B, C any](f func(A, B) C) func(A) func(B) C
description: Transforma una funcion de dos argumentos en forma currificada.
tags: ["functional","generic","curry"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/curry2.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 06d461bd0c80877548c818383cbb53b68000ef49978800d6b4336b4d37116c8d
example:
  ```go
  add := func(a, b int) int { return a + b }
  curriedAdd := Curry2(add)
  add5 := curriedAdd(5)
  result := add5(3) // 8
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Curry2 transforma una funcion de dos argumentos en forma currificada.
  func Curry2[A, B, C any](f func(A, B) C) func(A) func(B) C {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"f","desc":"función de dos argumentos a currificar: (A, B) -\u003e C"}],"output":"función currificada que retorna una función parcial: A -\u003e (B -\u003e C)"}


id: dag_parse_go_core
name: dag_parse
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func DagParse(data []byte) (DagDefinition, error)
tags: ["dag","yaml","parsing","workflow","dagu","pure"]
uses_functions: []
uses_types: ["dag_definition_go_core","dag_step_go_core","dag_handlers_go_core"]
returns: []
imports: ["fmt","strings","gopkg.in/yaml.v3"]
test_file_path: functions/core/dag_parse_test.go
file_path: functions/core/dag_parse.go
created_at: 2026-04-12T11:03:11Z
updated_at: 2026-04-12T11:03:11Z
props: []
emits: []
variant: []
content_hash: 2001c65d2651d7873e869e350ec29ac0c13fde467a99af63f31ae4d7b526ba1e
description:
  Parsea YAML de definicion de DAG en formato compatible con Dagu. Soporta schedule como string o lista, env como lista de maps single-key (formato Dagu), handler_on y handlers como aliases, steps con command/script/depends/continue_on, y type graph.

example:
  ```go
  data := []byte(`
  name: mi-dag
  schedule: "0 9 * * *"
  steps:
    - name: hello
      command: echo "hello"
    - name: world
      command: echo "world"
      depends: [hello]
  `)
  dag, err := DagParse(data)
  // dag.Name = "mi-dag"
  // dag.Schedule = ["0 9 * * *"]
  // dag.Steps[1].Depends = ["hello"]
  ```

tests:
  ["parsea DAG simple con steps y depends","parsea schedule como string y como lista","parsea env en formato lista de maps","parsea handler_on y handlers como alias","parsea continue_on y working_dir a nivel step","parsea type graph"]

notes:
  Funcion pura (el YAML es inmutable, no hay I/O). Internamente usa un struct rawDag para deserializar loosely y luego normaliza campos polimorficos. La estrategia de normalizacion: schedule string->[]string, env lista->map, handlers single-o-lista->[]DagStep. handler_on tiene precedencia sobre handlers si ambos estan presentes.

code:
  package core

  import (
  	"fmt"
  	"strings"

  	"gopkg.in/yaml.v3"
  )

  // rawDagStep is the loosely-typed intermediate representation of a DAG step.
  type rawDagStep struct {
  	Name        string            `yaml:"name"`
  	ID          string            `yaml:"id"`
  	Description string            `yaml:"description"`
  	Command     string            `yaml:"command"`
  	Script      string            `yaml:"script"`
  	Args        []string          `yaml:"args"`
  	Shell       string            `yaml:"shell"`
  	Dir         string            `yaml:"dir"`
  	WorkingDir  string            `yaml:"working_dir"`
  	Depends     []string          `yaml:"depends"`
  	Env         interface{}       `yaml:"env"`
  	ContinueOn  rawDagContinueOn  `yaml:"continue_on"`
  	RetryPolicy rawDagRetryPolicy `yaml:"retry_policy"`
  	TimeoutSec  int               `yaml:"timeout_sec"`
  	Output      string            `yaml:"output"`
  	Tags        []string          `yaml:"tags"`
  }

  type rawDagContinueOn struct {
  	Failure bool `yaml:"failure"`
  	Skipped bool `yaml:"skipped"`
  }

  type rawDagRetryPolicy struct {
  	Limit       int `yaml:"limit"`
  	IntervalSec int `yaml:"interval_sec"`
  }

  // rawDagHandlers handles both handler_on and handlers aliases.
  type rawDagHandlers struct {
  	Init    interface{} `yaml:"init"`
  	Success interface{} `yaml:"success"`
  	Failure interface{} `yaml:"failure"`
  	Exit    interface{} `yaml:"exit"`
  }

  // rawDag is the loosely-typed intermediate representation of a DAG YAML.
  type rawDag struct {
  	Name        string          `yaml:"name"`
  	Description string          `yaml:"description"`
  	Group       string          `yaml:"group"`
  	Type        string          `yaml:"type"`
  	WorkingDir  string          `yaml:"working_dir"`
  	Shell       string          `yaml:"shell"`
  	Env         interface{}     `yaml:"env"`
  	Schedule    interface{}     `yaml:"schedule"`
  	Steps       []rawDagStep    `yaml:"steps"`
  	HandlerOn   *rawDagHandlers `yaml:"handler_on"`
  	Handlers    *rawDagHandlers `yaml:"handlers"`
  	Tags        []string        `yaml:"tags"`
  	TimeoutSec  int             `yaml:"timeout_sec"`
  }

  // DagParse parses a YAML DAG definition compatible with Dagu format.
  // Handles schedule as string or list, env as list of single-key maps,
  // handler_on and handlers as aliases, and steps with command/script/depends.
  func DagParse(data []byte) (DagDefinition, error) {
  	var raw rawDag
  	if err := yaml.Unmarshal(data, &raw); err != nil {
  		return DagDefinition{}, fmt.Errorf("dag_parse: yaml unmarshal: %w", err)
  	}

  	def := DagDefinition{
  		Name:       raw.Name,
  		Description: raw.Description,
  		Group:      raw.Group,
  		Type:       raw.Type,
  		WorkingDir: raw.WorkingDir,
  		Shell:      raw.Shell,
  		Tags:       raw.Tags,
  		TimeoutSec: raw.TimeoutSec,
  	}

  	// Normalize env (list of single-key maps or plain map).
  	dagEnv, err := normalizeEnv(raw.Env)
  	if err != nil {
  		return DagDefinition{}, fmt.Errorf("dag_parse: env: %w", err)
  	}
  	def.Env = dagEnv

  	// Normalize schedule (string or list).
  	def.Schedule = normalizeSchedule(raw.Schedule)

  	// Normalize steps.
  	steps := make([]DagStep, 0, len(raw.Steps))
  	for i, rs := range raw.Steps {
  		step, err := normalizeStep(rs)
  		if err != nil {
  			return DagDefinition{}, fmt.Errorf("dag_parse: step[%d]: %w", i, err)
  		}
  		steps = append(steps, step)
  	}
  	def.Steps = steps

  	// Normalize handlers: handler_on takes precedence, fallback to handlers.
  	rawHandlers := raw.HandlerOn
  	if rawHandlers == nil {
  		rawHandlers = raw.Handlers
  	}
  	if rawHandlers != nil {
  		handlers, err := normalizeHandlers(rawHandlers)
  		if err != nil {
  			return DagDefinition{}, fmt.Errorf("dag_parse: handlers: %w", err)
  		}
  		def.HandlerOn = handlers
  	}

  	return def, nil
  }

  // normalizeSchedule converts schedule from string or []interface{} to []string.
  func normalizeSchedule(v interface{}) []string {
  	if v == nil {
  		return nil
  	}
  	switch t := v.(type) {
  	case string:
  		s := strings.TrimSpace(t)
  		if s == "" {
  			return nil
  		}
  		return []string{s}
  	case []interface{}:
  		result := make([]string, 0, len(t))
  		for _, item := range t {
  			if s, ok := item.(string); ok {
  				s = strings.TrimSpace(s)
  				if s != "" {
  					result = append(result, s)
  				}
  			}
  		}
  		return result
  	}
  	return nil
  }

  // normalizeEnv converts env from Dagu format (list of single-key maps) or plain map to map[string]string.
  func normalizeEnv(v interface{}) (map[string]string, error) {
  	if v == nil {
  		return nil, nil
  	}
  	switch t := v.(type) {
  	case map[string]interface{}:
  		// Plain YAML map.
  		result := make(map[string]string, len(t))
  		for k, val := range t {
  			result[k] = fmt.Sprintf("%v", val)
  		}
  		return result, nil
  	case []interface{}:
  		// Dagu format: list of single-key maps [KEY: value, KEY2: value2].
  		result := make(map[string]string)
  		for _, item := range t {
  			m, ok := item.(map[string]interface{})
  			if !ok {
  				continue
  			}
  			for k, val := range m {
  				result[k] = fmt.Sprintf("%v", val)
  			}
  		}
  		return result, nil
  	}
  	return nil, fmt.Errorf("unexpected env type %T", v)
  }

  // normalizeStep converts a rawDagStep to a DagStep.
  func normalizeStep(rs rawDagStep) (DagStep, error) {
  	stepEnv, err := normalizeEnv(rs.Env)
  	if err != nil {
  		return DagStep{}, fmt.Errorf("env: %w", err)
  	}

  	// working_dir is an alias for dir at the step level.
  	dir := rs.Dir
  	if dir == "" {
  		dir = rs.WorkingDir
  	}

  	return DagStep{
  		Name:        rs.Name,
  		ID:          rs.ID,
  		Description: rs.Description,
  		Command:     rs.Command,
  		Script:      rs.Script,
  		Args:        rs.Args,
  		Shell:       rs.Shell,
  		Dir:         dir,
  		Depends:     rs.Depends,
  		Env:         stepEnv,
  		ContinueOn: DagContinueOn{
  			Failure: rs.ContinueOn.Failure,
  			Skipped: rs.ContinueOn.Skipped,
  		},
  		RetryPolicy: DagRetryPolicy{
  			Limit:       rs.RetryPolicy.Limit,
  			IntervalSec: rs.RetryPolicy.IntervalSec,
  		},
  		TimeoutSec: rs.TimeoutSec,
  		Output:     rs.Output,
  		Tags:       rs.Tags,
  	}, nil
  }

  // normalizeHandlers converts rawDagHandlers to DagHandlers.
  // Each handler field can be a single step object or a list of steps.
  func normalizeHandlers(rh *rawDagHandlers) (DagHandlers, error) {
  	var h DagHandlers
  	var err error
  	h.Init, err = normalizeHandlerField(rh.Init)
  	if err != nil {
  		return DagHandlers{}, fmt.Errorf("init: %w", err)
  	}
  	h.Success, err = normalizeHandlerField(rh.Success)
  	if err != nil {
  		return DagHandlers{}, fmt.Errorf("success: %w", err)
  	}
  	h.Failure, err = normalizeHandlerField(rh.Failure)
  	if err != nil {
  		return DagHandlers{}, fmt.Errorf("failure: %w", err)
  	}
  	h.Exit, err = normalizeHandlerField(rh.Exit)
  	if err != nil {
  		return DagHandlers{}, fmt.Errorf("exit: %w", err)
  	}
  	return h, nil
  }

  // normalizeHandlerField converts a handler field (single step or list) to []DagStep.
  func normalizeHandlerField(v interface{}) ([]DagStep, error) {
  	if v == nil {
  		return nil, nil
  	}

  	// Re-marshal and unmarshal to handle polymorphic types cleanly.
  	b, err := yaml.Marshal(v)
  	if err != nil {
  		return nil, fmt.Errorf("re-marshal handler: %w", err)
  	}

  	// Try as a list first.
  	var rawList []rawDagStep
  	if err := yaml.Unmarshal(b, &rawList); err == nil && len(rawList) > 0 {
  		steps := make([]DagStep, 0, len(rawList))
  		for i, rs := range rawList {
  			step, err := normalizeStep(rs)
  			if err != nil {
  				return nil, fmt.Errorf("step[%d]: %w", i, err)
  			}
  			steps = append(steps, step)
  		}
  		return steps, nil
  	}

  	// Try as single step.
  	var rs rawDagStep
  	if err := yaml.Unmarshal(b, &rs); err != nil {
  		return nil, fmt.Errorf("unmarshal handler step: %w", err)
  	}
  	step, err := normalizeStep(rs)
  	if err != nil {
  		return nil, err
  	}
  	// If the step has no meaningful content, return nil.
  	if step.Name == "" && step.ID == "" && step.Command == "" && step.Script == "" {
  		return nil, nil
  	}
  	return []DagStep{step}, nil
  }

params_schema:
  {"params":[{"name":"data","desc":"contenido YAML de un archivo de definicion de DAG en formato Dagu"}],"output":"DagDefinition con todos los campos normalizados; error si el YAML es sintaticamente invalido"}


id: dag_resolve_env_go_core
name: dag_resolve_env
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func DagResolveEnv(dag DagDefinition, environ []string) DagDefinition
tags: ["dag","env","substitution","variable-expansion","pure"]
uses_functions: []
uses_types: ["dag_definition_go_core","dag_step_go_core"]
returns: []
imports: ["strings"]
tests: []
file_path: functions/core/dag_resolve_env.go
created_at: 2026-04-12T11:03:11Z
updated_at: 2026-04-12T11:03:11Z
props: []
emits: []
variant: []
content_hash: d771878139fe1331d3fa9d5e5f81c31922f9150af29b1423ef6bcdd9a223cd9e
description:
  Resuelve referencias a variables de entorno (${VAR} y $VAR) en un DagDefinition. Mergea environ del sistema con dag.Env (dag.Env tiene precedencia), y sustituye en Command, Script, Dir, Args y Env values de cada step. No muta el DagDefinition original.

example:
  ```go
  dag, _ := DagParse(yamlData)
  resolved := DagResolveEnv(dag, os.Environ())
  // resolved.Steps[0].Command tiene ${PROJECT_DIR} sustituido
  ```

notes:
  Funcion pura. No muta el DagDefinition de entrada — retorna una copia con los campos resueltos. Precedencia de variables: environ base < dag.Env < step.Env. Sustituye tanto ${VAR} (con llaves) como $VAR (bare). Si una variable no existe en el entorno, se deja sin sustituir. Los campos sustituidos son: DagDefinition.WorkingDir, Env values del DAG, y por step: Command, Script, Dir, Args, Env values.

code:
  package core

  import (
  	"strings"
  )

  // DagResolveEnv resolves environment variable references in a DagDefinition.
  // It parses environ (KEY=VALUE pairs from os.Environ()), merges with dag.Env
  // (dag.Env takes precedence), and substitutes ${VAR} and $VAR in Command,
  // Script, Dir, Args, and Env values of each step. Also substitutes in
  // DagDefinition.WorkingDir. Returns a new DagDefinition without mutating the original.
  func DagResolveEnv(dag DagDefinition, environ []string) DagDefinition {
  	// Parse environ into a base map.
  	base := parseEnviron(environ)

  	// Merge dag.Env over base (dag.Env has precedence).
  	dagEnv := mergeMaps(base, dag.Env)

  	// Build new DagDefinition (shallow copy, then replace fields).
  	result := dag

  	// Substitute in WorkingDir.
  	result.WorkingDir = substitute(dag.WorkingDir, dagEnv)

  	// Substitute in Env values of the DAG itself.
  	result.Env = substituteMap(dag.Env, dagEnv)

  	// Resolve steps.
  	resolvedSteps := make([]DagStep, len(dag.Steps))
  	for i, step := range dag.Steps {
  		// Merge dag env with step env (step env has precedence).
  		stepEnv := mergeMaps(dagEnv, step.Env)

  		rs := step
  		rs.Command = substitute(step.Command, stepEnv)
  		rs.Script = substitute(step.Script, stepEnv)
  		rs.Dir = substitute(step.Dir, stepEnv)

  		if len(step.Args) > 0 {
  			args := make([]string, len(step.Args))
  			for j, arg := range step.Args {
  				args[j] = substitute(arg, stepEnv)
  			}
  			rs.Args = args
  		}

  		rs.Env = substituteMap(step.Env, stepEnv)
  		resolvedSteps[i] = rs
  	}
  	result.Steps = resolvedSteps

  	return result
  }

  // parseEnviron parses a slice of "KEY=VALUE" strings into a map.
  func parseEnviron(environ []string) map[string]string {
  	m := make(map[string]string, len(environ))
  	for _, kv := range environ {
  		idx := strings.IndexByte(kv, '=')
  		if idx < 0 {
  			continue
  		}
  		m[kv[:idx]] = kv[idx+1:]
  	}
  	return m
  }

  // mergeMaps returns a new map with all entries from base, then overlaid with overlay.
  func mergeMaps(base, overlay map[string]string) map[string]string {
  	result := make(map[string]string, len(base)+len(overlay))
  	for k, v := range base {
  		result[k] = v
  	}
  	for k, v := range overlay {
  		result[k] = v
  	}
  	return result
  }

  // substituteMap applies substitute to all values in a map.
  func substituteMap(m map[string]string, env map[string]string) map[string]string {
  	if len(m) == 0 {
  		return m
  	}
  	result := make(map[string]string, len(m))
  	for k, v := range m {
  		result[k] = substitute(v, env)
  	}
  	return result
  }

  // substitute replaces ${VAR} and $VAR occurrences in s using the env map.
  func substitute(s string, env map[string]string) string {
  	if s == "" || !strings.Contains(s, "$") {
  		return s
  	}

  	var b strings.Builder
  	i := 0
  	for i < len(s) {
  		if s[i] != '$' {
  			b.WriteByte(s[i])
  			i++
  			continue
  		}

  		// Found '$'
  		i++
  		if i >= len(s) {
  			b.WriteByte('$')
  			break
  		}

  		if s[i] == '{' {
  			// ${VAR} form.
  			i++
  			end := strings.IndexByte(s[i:], '}')
  			if end < 0 {
  				// No closing brace — write as-is.
  				b.WriteString("${")
  				continue
  			}
  			varName := s[i : i+end]
  			i += end + 1
  			if val, ok := env[varName]; ok {
  				b.WriteString(val)
  			} else {
  				b.WriteString("${")
  				b.WriteString(varName)
  				b.WriteByte('}')
  			}
  		} else if isEnvVarChar(s[i]) {
  			// $VAR form (bare variable name).
  			start := i
  			for i < len(s) && isEnvVarChar(s[i]) {
  				i++
  			}
  			varName := s[start:i]
  			if val, ok := env[varName]; ok {
  				b.WriteString(val)
  			} else {
  				b.WriteByte('$')
  				b.WriteString(varName)
  			}
  		} else {
  			// Not a valid variable start — keep '$' and continue.
  			b.WriteByte('$')
  		}
  	}

  	return b.String()
  }

  // isEnvVarChar returns true for characters valid in environment variable names.
  func isEnvVarChar(c byte) bool {
  	return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'
  }

params_schema:
  {"params":[{"name":"dag","desc":"DagDefinition parseado con posibles referencias ${VAR} en sus campos"},{"name":"environ","desc":"lista de strings KEY=VALUE del entorno del sistema (tipicamente os.Environ())"}],"output":"nuevo DagDefinition con todas las referencias de variables sustituidas por sus valores"}


id: dag_topo_sort_go_core
name: dag_topo_sort
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func DagTopoSort(steps []DagStep) ([][]DagStep, error)
tags: ["dag","topological-sort","graph","kahn","pure"]
uses_functions: []
uses_types: ["dag_step_go_core"]
returns: []
imports: ["fmt"]
test_file_path: functions/core/dag_topo_sort_test.go
file_path: functions/core/dag_topo_sort.go
created_at: 2026-04-12T11:03:11Z
updated_at: 2026-04-12T11:03:11Z
props: []
emits: []
variant: []
content_hash: 03e99c66890e320dc06001090e822276fb95aabeb4994ed25d751bd0e39bd977
description:
  Ordenamiento topologico de steps de un DAG usando el algoritmo de Kahn. Retorna niveles donde cada nivel contiene steps que pueden ejecutarse en paralelo. Detecta ciclos y retorna error si los hay.

example:
  ```go
  steps := []DagStep{
      {Name: "a"},
      {Name: "b", Depends: []string{"a"}},
      {Name: "c", Depends: []string{"a"}},
      {Name: "d", Depends: []string{"b", "c"}},
  }
  levels, err := DagTopoSort(steps)
  // levels[0] = [a]
  // levels[1] = [b, c]   (paralelo)
  // levels[2] = [d]
  ```

tests:
  ["cadena lineal A-\u003eB-\u003eC produce tres niveles","diamond A-\u003eB A-\u003eC B-\u003eD C-\u003eD produce cuatro niveles","steps paralelos sin depends produce un solo nivel","ciclo A-\u003eB-\u003eA retorna error"]

notes:
  Funcion pura. Implementa Kahn's algorithm: calcula in-degree inicial, pone en cola los steps con in-degree 0, extrae un nivel completo por iteracion y reduce el in-degree de los dependientes. Si al terminar hay steps sin procesar, hay un ciclo. El orden dentro de cada nivel no esta garantizado — depende del orden del slice de entrada.

code:
  package core

  import "fmt"

  // DagTopoSort performs a topological sort of DAG steps using Kahn's algorithm.
  // Returns levels where each level contains steps that can run in parallel.
  // Returns an error if a dependency cycle is detected.
  func DagTopoSort(steps []DagStep) ([][]DagStep, error) {
  	if len(steps) == 0 {
  		return nil, nil
  	}

  	// Build index: ref -> DagStep.
  	index := make(map[string]DagStep, len(steps))
  	for _, s := range steps {
  		index[stepRef(s)] = s
  	}

  	// Compute in-degree for each step.
  	inDegree := make(map[string]int, len(steps))
  	for _, s := range steps {
  		ref := stepRef(s)
  		if _, ok := inDegree[ref]; !ok {
  			inDegree[ref] = 0
  		}
  		for range s.Depends {
  			inDegree[ref]++ // ref depends on a dep, so ref gets +1
  		}
  	}

  	// Build adjacency list: dep -> list of steps that depend on dep.
  	adj := make(map[string][]string, len(steps))
  	for _, s := range steps {
  		ref := stepRef(s)
  		for _, dep := range s.Depends {
  			adj[dep] = append(adj[dep], ref)
  		}
  	}

  	// Initialize queue with steps that have in-degree 0.
  	var queue []string
  	for _, s := range steps {
  		ref := stepRef(s)
  		if inDegree[ref] == 0 {
  			queue = append(queue, ref)
  		}
  	}

  	var levels [][]DagStep
  	processed := 0

  	for len(queue) > 0 {
  		// The current queue forms a parallel level.
  		level := make([]DagStep, 0, len(queue))
  		nextQueue := []string{}

  		for _, ref := range queue {
  			level = append(level, index[ref])
  			processed++
  			// Reduce in-degree for dependents.
  			for _, dependent := range adj[ref] {
  				inDegree[dependent]--
  				if inDegree[dependent] == 0 {
  					nextQueue = append(nextQueue, dependent)
  				}
  			}
  		}

  		levels = append(levels, level)
  		queue = nextQueue
  	}

  	if processed != len(steps) {
  		return nil, fmt.Errorf("dag_topo_sort: cycle detected (%d of %d steps processed)", processed, len(steps))
  	}

  	return levels, nil
  }

params_schema:
  {"params":[{"name":"steps","desc":"lista de DagStep con sus dependencias definidas en el campo Depends"}],"output":"slice de niveles donde cada nivel es un slice de DagStep que pueden ejecutarse en paralelo; error si hay ciclo"}


id: dag_validate_go_core
name: dag_validate
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func DagValidate(dag DagDefinition) DagValidationResult
tags: ["dag","validation","workflow","graph","pure"]
uses_functions: ["dag_topo_sort_go_core"]
uses_types: ["dag_definition_go_core","dag_validation_result_go_core"]
returns: []
imports: ["fmt"]
test_file_path: functions/core/dag_validate_test.go
file_path: functions/core/dag_validate.go
created_at: 2026-04-12T11:03:11Z
updated_at: 2026-04-12T11:03:11Z
props: []
emits: []
variant: []
content_hash: 35418e63ba6fb7bd9f37a0dbf2459f9279bbfcf50a00b2b16e42bc30f03cf5f7
description:
  Valida un DagDefinition para correcto uso estructural. Verifica que cada step tenga nombre o ID, que no haya duplicados, que todos los depends referencien steps existentes y que no haya ciclos (algoritmo de Kahn). Si el DAG es valido, calcula los niveles topologicos.

example:
  ```go
  dag, _ := DagParse(yamlData)
  res := DagValidate(dag)
  if !res.Valid {
      for _, e := range res.Errors {
          fmt.Println("ERROR:", e)
      }
  }
  // res.Levels = [["step-a"], ["step-b", "step-c"], ["step-d"]]
  ```

tests:
  ["DAG valido retorna Valid true y levels calculados","step sin nombre retorna error","depends a step inexistente retorna error","ciclo en dependencias retorna error"]

notes:
  Funcion pura. No modifica el DagDefinition de entrada. El calculo de niveles topologicos se delega a DagTopoSort para mantener la separacion de responsabilidades. Un warning (command+script simultaneos) no invalida el DAG.

code:
  package core

  import "fmt"

  // DagValidate validates a DagDefinition for structural correctness.
  // Checks: steps have name/ID, no duplicate names/IDs, all depends reference
  // existing steps, no dependency cycles. On success, computes topological levels.
  // Returns warnings for steps with both command and script set.
  func DagValidate(dag DagDefinition) DagValidationResult {
  	result := DagValidationResult{Valid: true}

  	// Build name/ID sets and check for missing identifiers and duplicates.
  	seen := make(map[string]bool)
  	for i, step := range dag.Steps {
  		ref := stepRef(step)
  		if ref == "" {
  			result.Errors = append(result.Errors,
  				fmt.Sprintf("step[%d]: must have name or id", i))
  			result.Valid = false
  			continue
  		}
  		if seen[ref] {
  			result.Errors = append(result.Errors,
  				fmt.Sprintf("step[%d]: duplicate name/id %q", i, ref))
  			result.Valid = false
  		}
  		seen[ref] = true

  		// Warning: command and script both set.
  		if step.Command != "" && step.Script != "" {
  			result.Warnings = append(result.Warnings,
  				fmt.Sprintf("step %q: has both command and script", ref))
  		}
  	}

  	if !result.Valid {
  		return result
  	}

  	// Check that all depends reference existing steps.
  	for _, step := range dag.Steps {
  		for _, dep := range step.Depends {
  			if !seen[dep] {
  				result.Errors = append(result.Errors,
  					fmt.Sprintf("step %q: depends on unknown step %q", stepRef(step), dep))
  				result.Valid = false
  			}
  		}
  	}

  	if !result.Valid {
  		return result
  	}

  	// Topological sort with Kahn's — detects cycles and computes levels.
  	levels, err := DagTopoSort(dag.Steps)
  	if err != nil {
  		result.Errors = append(result.Errors, fmt.Sprintf("cycle detected: %v", err))
  		result.Valid = false
  		return result
  	}

  	// Convert [][]DagStep to [][]string for the result.
  	strLevels := make([][]string, len(levels))
  	for i, level := range levels {
  		refs := make([]string, len(level))
  		for j, s := range level {
  			refs[j] = stepRef(s)
  		}
  		strLevels[i] = refs
  	}
  	result.Levels = strLevels

  	return result
  }

  // stepRef returns the canonical reference for a step (ID preferred, then Name).
  func stepRef(s DagStep) string {
  	if s.ID != "" {
  		return s.ID
  	}
  	return s.Name
  }

params_schema:
  {"params":[{"name":"dag","desc":"DagDefinition parseado y a validar"}],"output":"DagValidationResult con Valid=true si no hay errores, Errors con los problemas encontrados, Warnings con avisos no fatales, y Levels con los niveles topologicos si el DAG es valido"}


id: detect_cycle_go_core
name: detect_cycle
kind: function
lang: go
domain: core
version: 1.0.0
purity: impure
signature: func DetectCycle(conn *sql.DB, table, fromCol, toCol, filterCol, fromNode, toNode string) error
description: Detecta ciclos en un grafo dirigido almacenado en SQLite usando BFS antes de insertar una arista.
tags: ["graph","cycle","bfs","sqlite","validation"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql"]
tests: []
file_path: functions/core/detect_cycle.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d5425ca8bca0b16dc0ab00cb9fc3a2f6d435ffb46c4dbd61eb216c4540d66f72
example:
  ```go
  // Verificar si agregar A -> B crearia un ciclo en la tabla "relations"
  err := DetectCycle(db, "relations", "from_entity", "to_entity", "via", "A", "B")
  if err != nil {
      // ciclo detectado
  }

  // Sin filtro — considerar todas las aristas
  err = DetectCycle(db, "edges", "source", "target", "", "X", "Y")
  ```

notes:
  Usa BFS desde toNode siguiendo aristas existentes. Si alcanza fromNode, la nueva arista crearia un ciclo. El parametro filterCol permite ignorar aristas semanticas (no causales) — pasar "" para considerar todas.

code:
  package core

  import (
  	"database/sql"
  	"fmt"
  )

  // DetectCycle checks if adding a directed edge (from -> to) would create a cycle
  // in a directed graph stored in a SQLite table.
  // It performs BFS from toNode following edges where the filter column is non-empty.
  // If it reaches fromNode, a cycle exists.
  //
  // Parameters:
  //   - conn: open *sql.DB connection
  //   - table: table name containing the edges (e.g. "relations")
  //   - fromCol: column name for edge source (e.g. "from_entity")
  //   - toCol: column name for edge destination (e.g. "to_entity")
  //   - filterCol: column name that must be non-empty for causal edges (e.g. "via"); pass "" to consider all edges
  //   - fromNode: source node of the proposed new edge
  //   - toNode: destination node of the proposed new edge
  func DetectCycle(conn *sql.DB, table, fromCol, toCol, filterCol, fromNode, toNode string) error {
  	if fromNode == "" || toNode == "" {
  		return nil
  	}

  	var query string
  	if filterCol != "" {
  		query = fmt.Sprintf("SELECT %s FROM %s WHERE %s = ? AND %s != ''", toCol, table, fromCol, filterCol)
  	} else {
  		query = fmt.Sprintf("SELECT %s FROM %s WHERE %s = ?", toCol, table, fromCol)
  	}

  	visited := map[string]bool{}
  	queue := []string{toNode}

  	for len(queue) > 0 {
  		current := queue[0]
  		queue = queue[1:]

  		if visited[current] {
  			continue
  		}
  		visited[current] = true

  		if current == fromNode {
  			return fmt.Errorf("cycle detected: adding edge %s -> %s would create a cycle", fromNode, toNode)
  		}

  		rows, err := conn.Query(query, current)
  		if err != nil {
  			return fmt.Errorf("querying %s for cycle detection: %w", table, err)
  		}

  		for rows.Next() {
  			var next string
  			if err := rows.Scan(&next); err != nil {
  				rows.Close()
  				return err
  			}
  			if !visited[next] {
  				queue = append(queue, next)
  			}
  		}
  		rows.Close()
  	}

  	return nil
  }

params_schema:
  {"params":[{"name":"conn","desc":"conexión abierta a base de datos SQLite"},{"name":"table","desc":"nombre de la tabla que almacena el grafo"},{"name":"fromCol","desc":"nombre de la columna que contiene el nodo origen de cada arista"},{"name":"toCol","desc":"nombre de la columna que contiene el nodo destino de cada arista"},{"name":"filterCol","desc":"nombre de columna opcional para filtrar aristas semánticas; si es vacío, se consideran todas las aristas"},{"name":"fromNode","desc":"nodo origen de la nueva arista a validar"},{"name":"toNode","desc":"nodo destino de la nueva arista a validar"}],"output":"error si agregaría un ciclo; nil si no hay ciclo"}


id: drop_go_core
name: drop
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Drop[T any](slice []T, n int) []T
description: Elimina los primeros n elementos de un slice y devuelve el resto.
tags: ["slice","functional","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  result := Drop([]int{1, 2, 3, 4, 5}, 2)
  // result = [3, 4, 5]
  ```

tests: []
file_path: functions/core/drop.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 3c42dca338829597ee9cb90a55ae60a814b35e32718a47772a91615522436599
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Drop elimina los primeros n elementos de un slice y devuelve el resto.
  func Drop[T any](slice []T, n int) []T {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice del cual eliminar elementos del inicio"},{"name":"n","desc":"cantidad de elementos a eliminar desde el inicio"}],"output":"nuevo slice con los primeros n elementos eliminados; si n \u003e= len(slice), retorna slice vacío"}


id: filter_slice_go_core
name: filter_slice
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func FilterSlice[T any](xs []T, pred func(T) bool) []T
tags: ["slice","functional","generic","filter"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: ["filtra pares","slice vacio retorna vacio","ningun match retorna vacio"]
test_file_path: functions/core/filter_slice_test.go
file_path: functions/core/filter_slice.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a1d392c6d662a89c16e9250e5f57c8ea3f287ef16fa53cb1bdde052593022a9d
description:
  Filtra un slice aplicando un predicado sin mutar el original. Retorna un nuevo slice con los elementos que cumplen la condicion.

example:
  ```go
  evens := FilterSlice([]int{1, 2, 3, 4, 5}, func(n int) bool { return n%2 == 0 })
  // evens = [2, 4]
  ```

notes:
  Funcion pura generica. Crea un nuevo slice con capacidad pre-alocada al tamaño del original para minimizar reallocs.

code:
  package core

  // FilterSlice returns a new slice containing only elements where pred returns true.
  // Does not mutate the original slice.
  func FilterSlice[T any](xs []T, pred func(T) bool) []T {
  	result := make([]T, 0, len(xs))
  	for _, x := range xs {
  		if pred(x) {
  			result = append(result, x)
  		}
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"xs","desc":"slice de elementos a filtrar"},{"name":"pred","desc":"función predicado que retorna true para elementos a incluir"}],"output":"nuevo slice con solo los elementos que cumplen el predicado"}


id: find_go_core
name: find
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Find[T any](slice []T, predicate func(T) bool) Option[T]
description: Devuelve el primer elemento del slice que cumple el predicado, envuelto en Option.
tags: ["slice","functional","generic","search"]
uses_functions: []
uses_types: ["option_go_core"]
returns: ["option_go_core"]
imports: []
tests: []
file_path: functions/core/find.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 8888a74cee23130a898bf2e3a447472fef8cb90287f37d75691d73e75b6b7601
example:
  ```go
  nums := []int{1, 2, 3, 4, 5}
  result := Find(nums, func(n int) bool { return n > 3 })
  // result es Some(4)
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Find devuelve el primer elemento del slice que cumple el predicado, envuelto en Option.
  func Find[T any](slice []T, predicate func(T) bool) Option[T] {
  	// stub
  	return Option[T]{}
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice donde buscar"},{"name":"predicate","desc":"función que evalúa cada elemento; retorna true para el buscado"}],"output":"Option[T] envolviendo el primer elemento que cumple, o None si ninguno cumple"}


id: find_index_go_core
name: find_index
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func FindIndex[T any](slice []T, predicate func(T) bool) Option[int]
description: Devuelve el indice del primer elemento que cumple el predicado, envuelto en Option.
tags: ["slice","functional","generic","search"]
uses_functions: []
uses_types: ["option_go_core"]
returns: ["option_go_core"]
imports: []
example:
  ```go
  idx := FindIndex([]int{10, 20, 30}, func(x int) bool { return x > 15 })
  // idx = Some(1)
  ```

tests: []
file_path: functions/core/find_index.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: ca3a4dc3c32a37691eb8f8d7a8411e8f6044f6356242437509f4a703e733f14c
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // FindIndex devuelve el indice del primer elemento que cumple el predicado.
  func FindIndex[T any](slice []T, predicate func(T) bool) Option[int] {
  	// stub
  	return Option[int]{}
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice donde buscar"},{"name":"predicate","desc":"función que evalúa cada elemento; retorna true para el buscado"}],"output":"Option[int] envolviendo el índice del primer elemento que cumple, o None si ninguno cumple"}


id: flat_map_slice_go_core
name: flat_map_slice
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func FlatMapSlice[T, U any](slice []T, f func(T) []U) []U
description: Aplica una funcion que devuelve slices a cada elemento y aplana el resultado.
tags: ["slice","functional","generic","flatmap"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/flat_map_slice.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 739aa5dfbda7c6787b756cf61b242fa9d4e949f5b9f1cc5bdc6734ffafae042e
example:
  ```go
  result := FlatMapSlice([]int{1, 2, 3}, func(x int) []int { return []int{x, x * 10} })
  // result = [1, 10, 2, 20, 3, 30]
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // FlatMapSlice aplica una funcion que devuelve slices a cada elemento y aplana el resultado.
  func FlatMapSlice[T, U any](slice []T, f func(T) []U) []U {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice de elementos a transformar"},{"name":"f","desc":"función que transforma cada elemento en un slice de resultados"}],"output":"slice aplandado con todos los resultados concatenados"}


id: flatten_go_core
name: flatten
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Flatten[T any](slices [][]T) []T
description: Aplana un slice de slices en un unico slice.
tags: ["slice","functional","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  result := Flatten([][]int{{1, 2}, {3, 4}, {5}})
  // result = [1, 2, 3, 4, 5]
  ```

tests: []
file_path: functions/core/flatten.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 6a9cbe0eba7c1940895e4bfede3661b612f6751f4dddc89f2f8b5c4e8451efe2
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Flatten aplana un slice de slices en un unico slice.
  func Flatten[T any](slices [][]T) []T {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"slices","desc":"slice de slices a aplanar"}],"output":"slice único con todos los elementos concatenados preservando orden"}


id: flip_go_core
name: flip
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Flip[A, B, C any](f func(A, B) C) func(B, A) C
description: Intercambia el orden de los argumentos de una funcion de dos parametros.
tags: ["functional","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/flip.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 593a6802a313fb304c5533f44acfd821fd2bdd90ecfae2076404aac30cd490bd
example:
  ```go
  sub := func(a, b int) int { return a - b }
  flipped := Flip(sub)
  result := flipped(3, 10) // 7 (10 - 3)
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Flip intercambia el orden de los argumentos de una funcion de dos parametros.
  func Flip[A, B, C any](f func(A, B) C) func(B, A) C {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"f","desc":"función de dos argumentos a invertir: (A, B) -\u003e C"}],"output":"función con argumentos intercambiados: (B, A) -\u003e C"}


id: generate_id_go_core
name: generate_id
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func GenerateID(name, lang, domain string) string
description: Genera un ID canonico determinista a partir de nombre, lenguaje y dominio.
tags: ["id","naming","deterministic"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  id := GenerateID("filter_slice", "go", "core")
  // id = "filter_slice_go_core"
  ```

tests: []
file_path: functions/core/generate_id.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d12138b7e3bf12f3e623a2323fbccfe8698345272df951a1455e2f7646d80960
notes:
  Funcion pura sin dependencias. Util para cualquier sistema que necesite IDs compuestos deterministas a partir de componentes con nombre.

code:
  package core

  // GenerateID builds a canonical ID from name, lang, and domain.
  // Format: {name}_{lang}_{domain}
  func GenerateID(name, lang, domain string) string {
  	return name + "_" + lang + "_" + domain
  }

params_schema:
  {"params":[{"name":"name","desc":"nombre de la entidad en snake_case"},{"name":"lang","desc":"lenguaje: 'go', 'py', 'bash', 'ts'"},{"name":"domain","desc":"dominio de la función: 'core', 'finance', etc."}],"output":"ID canonizado en formato 'name_lang_domain' convertido a minúsculas"}


id: group_by_go_core
name: group_by
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func GroupBy[T any, K comparable](slice []T, keyFn func(T) K) map[K][]T
description: Agrupa elementos de un slice por clave generada con una funcion.
tags: ["slice","functional","generic","grouping"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/group_by.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 3213f93031cf67873f31b97caed0c0e9eb2d81dc15b52f05011b03e1cfb5be2b
example:
  ```go
  groups := GroupBy([]string{"go", "git", "python"}, func(s string) byte { return s[0] })
  // groups = map[byte][]string{'g': {"go", "git"}, 'p': {"python"}}
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // GroupBy agrupa elementos de un slice por clave generada con una funcion.
  func GroupBy[T any, K comparable](slice []T, keyFn func(T) K) map[K][]T {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice de elementos a agrupar"},{"name":"keyFn","desc":"función que extrae la clave de agrupación para cada elemento"}],"output":"mapa donde cada clave única mapea a un slice de elementos con esa clave"}


id: identity_go_core
name: identity
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Identity[T any](x T) T
description: Devuelve el valor recibido sin modificarlo. Elemento neutro de la composicion.
tags: ["functional","generic","identity"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  x := Identity(42) // 42
  s := Identity("abc") // "abc"
  ```

tests: []
file_path: functions/core/identity.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: a1b0425f675649dbf7d419785f9122a3e13092b31a1c88d3f66267a58876960b
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Identity devuelve el valor recibido sin modificarlo.
  func Identity[T any](x T) T {
  	// stub
  	var zero T
  	return zero
  }

params_schema:
  {"params":[{"name":"x","desc":"valor a retornar sin modificación"}],"output":"el mismo valor recibido como entrada"}


id: join_by_key_go_core
name: join_by_key
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func JoinByKey(left, right []map[string]any, key, how string) []map[string]any
tags: ["tabular","join","merge","go","core"]
uses_functions: []
uses_types: []
returns: []
imports: []
test_file_path: functions/core/join_by_key_test.go
file_path: functions/core/join_by_key.go
created_at: 2026-04-05T14:44:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 78d327ee5eabefd02c9225ad1c57730e9dc0da02306f709bdefa55b8393d0b33
description:
  Join de dos slices de map[string]any por una clave comun. Soporta inner, left, right y outer. Campos duplicados del right se sufijan con _right. Algoritmo O(n+m).

example:
  ```go
  left := []map[string]any{{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}}
  right := []map[string]any{{"id": 1, "dept": "eng"}, {"id": 3, "dept": "sales"}}

  result := JoinByKey(left, right, "id", "inner")
  // [{"id": 1, "name": "Alice", "dept": "eng"}]

  result = JoinByKey(left, right, "id", "left")
  // [{"id": 1, "name": "Alice", "dept": "eng"},
  //  {"id": 2, "name": "Bob", "dept": nil}]
  ```

tests:
  ["Inner join solo matches","Left join todos los left con nil para right sin match","Right join","Outer join","Campos duplicados con sufijo _right","Key ausente en alguna fila"]

notes:
  Funcion pura sin dependencias externas.
  El algoritmo indexa right en O(n) y luego itera left en O(m), total O(n+m).
  Los campos de right que colisionan con campos de left (excepto la clave) se renombran con sufijo _right.
  Un key puede tener multiples filas en right — se generan multiples filas en el resultado (comportamiento de join relacional).

code:
  package core

  // JoinByKey une dos slices de map[string]any por una clave comun.
  // Soporta los cuatro tipos de join: inner, left, right, outer.
  // Campos duplicados del lado right (distintos a la clave) se sufijan con _right.
  // Algoritmo O(n+m): indexa right por key, luego itera left.
  func JoinByKey(left, right []map[string]any, key, how string) []map[string]any {
  	// Determinar campos conflictivos entre left y right
  	leftFields := map[string]bool{}
  	for _, row := range left {
  		for k := range row {
  			leftFields[k] = true
  		}
  	}
  	rightFields := map[string]bool{}
  	for _, row := range right {
  		for k := range row {
  			if k != key {
  				rightFields[k] = true
  			}
  		}
  	}
  	conflicting := map[string]bool{}
  	for k := range rightFields {
  		if leftFields[k] {
  			conflicting[k] = true
  		}
  	}

  	// Indexar right por key (un key puede tener multiples rows)
  	rightIndex := map[any][]map[string]any{}
  	for _, row := range right {
  		k := row[key]
  		rightIndex[k] = append(rightIndex[k], row)
  	}

  	// Plantilla vacia del right (todos los campos de right a nil)
  	emptyRight := func() map[string]any {
  		m := map[string]any{}
  		for k := range rightFields {
  			if conflicting[k] {
  				m[k+"_right"] = nil
  			} else {
  				m[k] = nil
  			}
  		}
  		return m
  	}

  	merge := func(l, r map[string]any) map[string]any {
  		out := map[string]any{}
  		if l != nil {
  			for k, v := range l {
  				out[k] = v
  			}
  		}
  		if r != nil {
  			for k, v := range r {
  				if k == key {
  					continue
  				}
  				if conflicting[k] {
  					out[k+"_right"] = v
  				} else {
  					out[k] = v
  				}
  			}
  		}
  		return out
  	}

  	matchedRightKeys := map[any]bool{}
  	var result []map[string]any

  	for _, l := range left {
  		k := l[key]
  		rRows, ok := rightIndex[k]
  		if ok {
  			matchedRightKeys[k] = true
  			for _, r := range rRows {
  				result = append(result, merge(l, r))
  			}
  		} else {
  			if how == "left" || how == "outer" {
  				row := merge(l, nil)
  				for rk, rv := range emptyRight() {
  					row[rk] = rv
  				}
  				result = append(result, row)
  			}
  		}
  	}

  	if how == "right" || how == "outer" {
  		for _, r := range right {
  			k := r[key]
  			if !matchedRightKeys[k] {
  				row := emptyRight()
  				row[key] = k
  				for rk, rv := range r {
  					if rk == key {
  						continue
  					}
  					if conflicting[rk] {
  						row[rk+"_right"] = rv
  					} else {
  						row[rk] = rv
  					}
  				}
  				result = append(result, row)
  			}
  		}
  	}

  	return result
  }

params_schema:
  {"params":[{"name":"left","desc":"slice izquierdo de map[string]any a unir"},{"name":"right","desc":"slice derecho de map[string]any a unir"},{"name":"key","desc":"nombre del campo común a usar como clave de unión"},{"name":"how","desc":"tipo de unión: 'inner', 'left', 'right', 'outer'"}],"output":"slice de map[string]any con el resultado de la unión; campos duplicados del right se sufijan con '_right'"}


id: longest_common_prefix_go_core
name: longest_common_prefix
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func LongestCommonPrefix(items []string) string
tags: ["core","string","prefix","autocomplete","common"]
uses_functions: []
uses_types: []
returns: []
imports: ["strings"]
tests: []
file_path: functions/core/longest_common_prefix.go
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: f682ff54c8918e2fbc13ad13918ad7f407e3fefc34595c114a89d8856e8e076f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: launcher/core/commands.go
description:
  Retorna el prefijo comun mas largo compartido por todos los strings del slice. Util para autocompletado de rutas y comandos.

example:
  ```go
  prefix := core.LongestCommonPrefix([]string{"/home/user/docs", "/home/user/downloads", "/home/user/desktop"})
  // prefix == "/home/user/d"

  empty := core.LongestCommonPrefix([]string{"abc", "xyz"})
  // empty == ""
  ```

notes:
  Compara caracter por caracter acortando el prefijo progresivamente. Complejidad O(n*m) donde n es el numero de strings y m la longitud del prefijo.

code:
  package core

  import "strings"

  // LongestCommonPrefix returns the longest string that is a prefix of every item.
  // Returns "" if the slice is empty or no common prefix exists.
  func LongestCommonPrefix(items []string) string {
  	if len(items) == 0 {
  		return ""
  	}
  	prefix := items[0]
  	for _, item := range items[1:] {
  		for !strings.HasPrefix(item, prefix) {
  			if prefix == "" {
  				return ""
  			}
  			prefix = prefix[:len(prefix)-1]
  		}
  	}
  	return prefix
  }

params_schema:
  {"params":[{"name":"items","desc":"slice de strings para buscar prefijo comun"}],"output":"prefijo comun mas largo, o vacio si no hay coincidencia"}


id: map_concurrent_go_core
name: map_concurrent
kind: function
lang: go
domain: core
version: 1.0.0
purity: impure
signature: func MapConcurrent[T any, U any](xs []T, fn func(T) U, workers int) []U
tags: ["map","concurrent","parallel","generic"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: functions/core/map_concurrent.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 70e39ed89fef91d7ed0260541491efe459b66f0a1d071c1cfbbb60d6b68063a7
description:
  Aplica una funcion a cada elemento de un slice usando un pool de goroutines como workers. Los resultados preservan el orden original del slice de entrada.

example:
  ```go
  squares := MapConcurrent([]int{1, 2, 3, 4, 5}, func(n int) int {
      return n * n
  }, 3)
  // squares = [1, 4, 9, 16, 25] (orden preservado)
  ```

notes:
  Funcion impura generica que usa goroutines y sync.WaitGroup. Los workers se alimentan de un canal con indices, garantizando que cada resultado se escribe en su posicion correcta sin race conditions (cada goroutine escribe en un indice unico). Si workers <= 0 se usa 1. Si workers > len(xs) se ajusta a len(xs).

code:
  package core

  import "sync"

  // MapConcurrent applies fn to each element of xs using a pool of worker goroutines.
  // The number of concurrent workers is capped by the workers parameter.
  // Results preserve the original index order. If workers <= 0, it defaults to 1.
  func MapConcurrent[T any, U any](xs []T, fn func(T) U, workers int) []U {
  	if workers <= 0 {
  		workers = 1
  	}
  	n := len(xs)
  	if n == 0 {
  		return []U{}
  	}

  	results := make([]U, n)
  	var wg sync.WaitGroup
  	ch := make(chan int, n)

  	// Feed indices into the channel.
  	for i := range xs {
  		ch <- i
  	}
  	close(ch)

  	// Spawn workers.
  	if workers > n {
  		workers = n
  	}
  	wg.Add(workers)
  	for w := 0; w < workers; w++ {
  		go func() {
  			defer wg.Done()
  			for i := range ch {
  				results[i] = fn(xs[i])
  			}
  		}()
  	}

  	wg.Wait()
  	return results
  }

params_schema:
  {"params":[{"name":"xs","desc":"slice de elementos a procesar"},{"name":"fn","desc":"función a aplicar a cada elemento"},{"name":"workers","desc":"número de goroutines concurrentes; se ajusta a max(1, min(workers, len(xs)))"}],"output":"slice de resultados en el mismo orden original que la entrada"}


id: map_slice_go_core
name: map_slice
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func MapSlice[T any, U any](xs []T, fn func(T) U) []U
tags: ["slice","functional","generic","map","transform"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: ["transforma enteros a strings","slice vacio retorna vacio","preserva orden"]
test_file_path: functions/core/map_slice_test.go
file_path: functions/core/map_slice.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 59b061d800d871dbe39512872287a8c72ca78cd167bd35ff932ec431a0983d47
description:
  Transforma cada elemento de un slice aplicando una funcion. Retorna un nuevo slice del mismo tamaño con los resultados.

example:
  ```go
  strs := MapSlice([]int{1, 2, 3}, func(n int) string { return fmt.Sprintf("%d", n) })
  // strs = ["1", "2", "3"]
  ```

notes:
  Funcion pura generica con dos type parameters: T (input) y U (output). Pre-aloca el slice resultado al tamaño exacto.

code:
  package core

  // MapSlice applies fn to each element of xs and returns a new slice with the results.
  // Does not mutate the original slice.
  func MapSlice[T any, U any](xs []T, fn func(T) U) []U {
  	result := make([]U, len(xs))
  	for i, x := range xs {
  		result[i] = fn(x)
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"xs","desc":"slice de elementos a transformar"},{"name":"fn","desc":"función que transforma cada elemento de tipo T a tipo U"}],"output":"nuevo slice de resultados transformados, mismo tamaño que entrada"}


id: memoize_go_core
name: memoize
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Memoize[K comparable, V any](fn func(K) V) func(K) V
tags: ["cache","memoize","functional","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/memoize.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 21b5c3e8ea8e4bae48308b5e5870258aeac81574f67347af7d1d5f5a5fb43c74
description:
  Cachea resultados de una funcion pura. Retorna una nueva funcion que almacena en un mapa interno los resultados ya calculados, evitando recalculos para la misma clave.

example:
  ```go
  fib := Memoize(func(n int) int {
      if n <= 1 {
          return n
      }
      // nota: para recursion memoizada se necesita declarar la variable antes
      return n // simplificado
  })
  doubled := Memoize(func(x int) int { return x * 2 })
  doubled(5) // calcula: 10
  doubled(5) // cache hit: 10
  ```

notes:
  Funcion pura generica (referencialmente transparente). El cache interno es un map[K]V sin sincronizacion, por lo que la funcion retornada es segura solo para uso en una sola goroutine. K debe ser comparable para usarse como clave del mapa.

code:
  package core

  // Memoize wraps a pure function fn so that results are cached by key.
  // Subsequent calls with the same key return the cached value without calling fn again.
  // The returned function is safe for single-goroutine use.
  func Memoize[K comparable, V any](fn func(K) V) func(K) V {
  	cache := make(map[K]V)
  	return func(key K) V {
  		if val, ok := cache[key]; ok {
  			return val
  		}
  		val := fn(key)
  		cache[key] = val
  		return val
  	}
  }

params_schema:
  {"params":[{"name":"fn","desc":"función pura a memorizar: K -\u003e V; los resultados se cachean por clave"}],"output":"función memoizada que retorna resultados en caché para claves ya calculadas; no es thread-safe"}


id: next_cron_time_go_core
name: next_cron_time
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func NextCronTime(schedule CronSchedule, after time.Time) time.Time
tags: ["cron","scheduling","time","next","pure"]
uses_functions: ["parse_cron_expr_go_core"]
uses_types: ["cron_schedule_go_core"]
returns: []
imports: ["time"]
test_file_path: functions/core/next_cron_time_test.go
file_path: functions/core/next_cron_time.go
created_at: 2026-04-05T14:44:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7801d848b125cfb6b459efe9745e455fc2ee8ea44ca3496593cb8ff916c51621
description:
  Calcula la proxima ejecucion de un cron schedule despues de un tiempo dado. Avanza minuto a minuto saltando campos no coincidentes. Retorna zero time si no hay match en 366 dias (schedule imposible).

example:
  ```go
  sched, _ := ParseCronExpr("0 * * * *")
  after := time.Date(2024, 1, 15, 14, 30, 0, 0, time.UTC)
  next := NextCronTime(sched, after)
  // next = 2024-01-15 15:00:00 UTC

  weekdays, _ := ParseCronExpr("0 9 * * 1-5")
  friday := time.Date(2024, 1, 19, 10, 0, 0, 0, time.UTC)  // Friday
  next2 := NextCronTime(weekdays, friday)
  // next2 = 2024-01-22 09:00:00 UTC (Monday)
  ```

tests:
  ["0 * * * * desde :30 retorna la proxima hora en punto","@weekly desde viernes retorna proximo domingo a medianoche","0 9 * * 1-5 desde viernes retorna proximo lunes a las 9","schedule imposible retorna zero time"]

notes:
  Usa semantica AND para day_of_month y day_of_week: ambos campos deben coincidir. El limite de 366 dias evita loops infinitos en schedules imposibles (ej: 29 de febrero en un ano sin bisiesto). Devuelve zero time en lugar de error para mantener purity: false/zero es el idiom de Go para retornos opcionales sin error.

code:
  package core

  import (
  	"time"
  )

  // NextCronTime returns the next time.Time that satisfies schedule after the given time.
  // It advances minute by minute, skipping ahead when a field does not match.
  // Returns the zero value of time.Time if no match is found within 366 days (impossible schedule).
  func NextCronTime(schedule CronSchedule, after time.Time) time.Time {
  	// Truncate to minute, then advance by 1 minute.
  	t := after.Truncate(time.Minute).Add(time.Minute)

  	limit := after.Add(366 * 24 * time.Hour)

  	for t.Before(limit) {
  		// Check month (1-12).
  		if !intIn(int(t.Month()), schedule.Month) {
  			// Advance to first day of next valid month.
  			t = nextValidMonth(t, schedule.Month)
  			if t.IsZero() {
  				return time.Time{}
  			}
  			continue
  		}

  		// Check day of month AND day of week (cron uses OR semantics when both are restricted,
  		// but standard 5-field cron: if both are non-wildcard, either can match).
  		// For simplicity we use AND semantics (both must match) which is the POSIX default
  		// for the common case; most implementations differ only when both are explicitly set.
  		domOK := intIn(t.Day(), schedule.DayOfMonth)
  		dowOK := intIn(int(t.Weekday()), schedule.DayOfWeek)
  		if !domOK || !dowOK {
  			// Advance to next day at midnight.
  			t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
  			continue
  		}

  		// Check hour.
  		if !intIn(t.Hour(), schedule.Hour) {
  			// Advance to next valid hour.
  			next := nextValidHour(t, schedule.Hour)
  			if next.IsZero() {
  				// No valid hour today; advance to tomorrow.
  				t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
  			} else {
  				t = next
  			}
  			continue
  		}

  		// Check minute.
  		if !intIn(t.Minute(), schedule.Minute) {
  			next := nextValidMinute(t, schedule.Minute)
  			if next.IsZero() {
  				// No more valid minutes this hour; advance to next hour.
  				t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, 0, 0, 0, t.Location())
  			} else {
  				t = next
  			}
  			continue
  		}

  		// All fields match.
  		return t
  	}

  	return time.Time{}
  }

  // intIn returns true if v is in the sorted slice s.
  func intIn(v int, s []int) bool {
  	for _, x := range s {
  		if x == v {
  			return true
  		}
  	}
  	return false
  }

  // nextValidMonth advances t to the first moment of the next valid month.
  func nextValidMonth(t time.Time, months []int) time.Time {
  	month := int(t.Month())
  	for _, m := range months {
  		if m > month {
  			return time.Date(t.Year(), time.Month(m), 1, 0, 0, 0, 0, t.Location())
  		}
  	}
  	// Wrap to next year.
  	if len(months) > 0 {
  		return time.Date(t.Year()+1, time.Month(months[0]), 1, 0, 0, 0, 0, t.Location())
  	}
  	return time.Time{}
  }

  // nextValidHour returns t at the next valid hour this day, or zero if none.
  func nextValidHour(t time.Time, hours []int) time.Time {
  	h := t.Hour()
  	for _, hh := range hours {
  		if hh > h {
  			return time.Date(t.Year(), t.Month(), t.Day(), hh, 0, 0, 0, t.Location())
  		}
  	}
  	return time.Time{}
  }

  // nextValidMinute returns t at the next valid minute this hour, or zero if none.
  func nextValidMinute(t time.Time, minutes []int) time.Time {
  	m := t.Minute()
  	for _, mm := range minutes {
  		if mm > m {
  			return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), mm, 0, 0, t.Location())
  		}
  	}
  	return time.Time{}
  }

params_schema:
  {"params":[{"name":"schedule","desc":"CronSchedule con listas de minutos, horas, días, etc. válidos"},{"name":"after","desc":"tiempo a partir del cual buscar la próxima ejecución"}],"output":"time.Time con la próxima ejecución; zero time si no hay match dentro de 366 días"}


id: parse_cron_expr_go_core
name: parse_cron_expr
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func ParseCronExpr(expr string) (CronSchedule, error)
tags: ["cron","scheduling","parsing","time","pure"]
uses_functions: []
uses_types: ["cron_schedule_go_core"]
returns: []
imports: ["fmt","strconv","strings"]
test_file_path: functions/core/parse_cron_expr_test.go
file_path: functions/core/parse_cron_expr.go
created_at: 2026-04-05T11:24:20Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 352cd1f4614895e86bd5bb96b2946c82658f1cc52ce6690a18ccbc0a7846be4b
description:
  Parsea una expresion cron estandar de 5 campos en un CronSchedule con valores expandidos. Soporta *, rangos (1-5), listas (1,3,5), pasos (*/15) y aliases (@hourly, @daily, @weekly, @monthly, @yearly). No soporta segundos ni years estilo Quartz.

example:
  ```go
  sched, err := ParseCronExpr("*/15 * * * *")
  // sched.Minute = [0, 15, 30, 45]
  // sched.Hour   = [0, 1, ..., 23]

  sched2, _ := ParseCronExpr("@daily")
  // sched2.Minute = [0], sched2.Hour = [0]

  sched3, _ := ParseCronExpr("0 9 * * 1-5")
  // sched3.DayOfWeek = [1, 2, 3, 4, 5]  (lunes a viernes)
  ```

tests:
  ["*/15 expande minutos a [0 15 30 45]","@daily resuelve a 0 0 en todos los campos restantes","0 9 1,15 * * expande dias a [1 15]","0 9 * * 1-5 expande dia de semana a [1 2 3 4 5]","expresion con 4 campos retorna error","minuto fuera de rango retorna error"]

notes:
  Funcion pura. Cada campo cron se expande a la lista completa de valores enteros validos. Los aliases se resuelven antes del parseo. Los limites son: minute [0,59], hour [0,23], day_of_month [1,31], month [1,12], day_of_week [0,6] (0=domingo).

code:
  package core

  import (
  	"fmt"
  	"strconv"
  	"strings"
  )

  // aliases maps cron shorthand expressions to their 5-field equivalents.
  var cronAliases = map[string]string{
  	"@yearly":   "0 0 1 1 *",
  	"@annually": "0 0 1 1 *",
  	"@monthly":  "0 0 1 * *",
  	"@weekly":   "0 0 * * 0",
  	"@daily":    "0 0 * * *",
  	"@midnight": "0 0 * * *",
  	"@hourly":   "0 * * * *",
  }

  // fieldLimits defines the valid [min, max] range for each cron field.
  var cronFieldLimits = [5][2]int{
  	{0, 59},  // minute
  	{0, 23},  // hour
  	{1, 31},  // day of month
  	{1, 12},  // month
  	{0, 6},   // day of week
  }

  var cronFieldNames = [5]string{"minute", "hour", "day_of_month", "month", "day_of_week"}

  // ParseCronExpr parses a standard 5-field cron expression into a CronSchedule.
  // Supports *, ranges (1-5), lists (1,3,5), steps (*/15), and aliases (@hourly, @daily, @weekly, @monthly, @yearly).
  // Returns an error for invalid expressions or out-of-range values.
  func ParseCronExpr(expr string) (CronSchedule, error) {
  	expr = strings.TrimSpace(expr)

  	// Resolve aliases.
  	if expanded, ok := cronAliases[expr]; ok {
  		expr = expanded
  	}

  	fields := strings.Fields(expr)
  	if len(fields) != 5 {
  		return CronSchedule{}, fmt.Errorf("parse_cron_expr: expected 5 fields, got %d in %q", len(fields), expr)
  	}

  	var result [5][]int
  	for i, field := range fields {
  		lo, hi := cronFieldLimits[i][0], cronFieldLimits[i][1]
  		values, err := parseCronField(field, lo, hi)
  		if err != nil {
  			return CronSchedule{}, fmt.Errorf("parse_cron_expr: field %s: %w", cronFieldNames[i], err)
  		}
  		result[i] = values
  	}

  	return CronSchedule{
  		Minute:     result[0],
  		Hour:       result[1],
  		DayOfMonth: result[2],
  		Month:      result[3],
  		DayOfWeek:  result[4],
  		Raw:        strings.TrimSpace(strings.Join(fields, " ")),
  	}, nil
  }

  // parseCronField expands a single cron field token into the list of matching integers.
  func parseCronField(field string, lo, hi int) ([]int, error) {
  	// Handle wildcard.
  	if field == "*" {
  		return rangeSlice(lo, hi), nil
  	}

  	var values []int
  	seen := make(map[int]bool)

  	// Handle comma-separated list.
  	parts := strings.Split(field, ",")
  	for _, part := range parts {
  		expanded, err := parseCronPart(part, lo, hi)
  		if err != nil {
  			return nil, err
  		}
  		for _, v := range expanded {
  			if !seen[v] {
  				seen[v] = true
  				values = append(values, v)
  			}
  		}
  	}

  	// Sort.
  	sortInts(values)
  	return values, nil
  }

  // parseCronPart handles a single part: plain int, range (a-b), or step (*/n or a-b/n).
  func parseCronPart(part string, lo, hi int) ([]int, error) {
  	// Step: */n or a-b/n
  	if idx := strings.Index(part, "/"); idx != -1 {
  		stepStr := part[idx+1:]
  		step, err := strconv.Atoi(stepStr)
  		if err != nil || step <= 0 {
  			return nil, fmt.Errorf("invalid step %q", stepStr)
  		}
  		base := part[:idx]
  		var start, end int
  		if base == "*" {
  			start, end = lo, hi
  		} else if dashIdx := strings.Index(base, "-"); dashIdx != -1 {
  			var err2 error
  			start, end, err2 = parseRange(base, lo, hi)
  			if err2 != nil {
  				return nil, err2
  			}
  		} else {
  			v, err2 := parseValue(base, lo, hi)
  			if err2 != nil {
  				return nil, err2
  			}
  			start, end = v, hi
  		}
  		var result []int
  		for v := start; v <= end; v += step {
  			result = append(result, v)
  		}
  		return result, nil
  	}

  	// Range: a-b
  	if strings.Contains(part, "-") {
  		start, end, err := parseRange(part, lo, hi)
  		if err != nil {
  			return nil, err
  		}
  		return rangeSlice(start, end), nil
  	}

  	// Plain integer.
  	v, err := parseValue(part, lo, hi)
  	if err != nil {
  		return nil, err
  	}
  	return []int{v}, nil
  }

  func parseRange(s string, lo, hi int) (int, int, error) {
  	parts := strings.SplitN(s, "-", 2)
  	if len(parts) != 2 {
  		return 0, 0, fmt.Errorf("invalid range %q", s)
  	}
  	start, err := parseValue(parts[0], lo, hi)
  	if err != nil {
  		return 0, 0, err
  	}
  	end, err := parseValue(parts[1], lo, hi)
  	if err != nil {
  		return 0, 0, err
  	}
  	if start > end {
  		return 0, 0, fmt.Errorf("range start %d > end %d", start, end)
  	}
  	return start, end, nil
  }

  func parseValue(s string, lo, hi int) (int, error) {
  	v, err := strconv.Atoi(s)
  	if err != nil {
  		return 0, fmt.Errorf("invalid value %q: not an integer", s)
  	}
  	if v < lo || v > hi {
  		return 0, fmt.Errorf("value %d out of range [%d, %d]", v, lo, hi)
  	}
  	return v, nil
  }

  func rangeSlice(lo, hi int) []int {
  	s := make([]int, hi-lo+1)
  	for i := range s {
  		s[i] = lo + i
  	}
  	return s
  }

  // sortInts is a simple insertion sort for small slices (avoids importing sort).
  func sortInts(a []int) {
  	for i := 1; i < len(a); i++ {
  		for j := i; j > 0 && a[j] < a[j-1]; j-- {
  			a[j], a[j-1] = a[j-1], a[j]
  		}
  	}
  }

params_schema:
  {"params":[{"name":"expr","desc":"expresión cron de 5 campos: minuto hora día_mes mes día_semana; soporta *, rangos, listas, pasos y aliases"}],"output":"CronSchedule con listas de valores válidos por campo; error si la expresión es inválida"}


id: parse_version_go_core
name: parse_version
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func ParseVersion(content string) string
tags: ["core","version","parse","text","extract"]
uses_functions: []
uses_types: []
returns: []
imports: ["strings"]
tests: []
file_path: functions/core/parse_version.go
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 6df92a3bafc28e72943a9bc6d2aa65004be4483ed12c4643cf7021240371e1dd
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: installer/core/version.go
description:
  Extrae el tag de version del primer campo de la primera linea de texto. Util para parsear archivos VERSION.txt o salida de comandos --version.

example:
  ```go
  v := core.ParseVersion("v0.4.9 - Instalador Julia Windows (2026-02-28)")
  // v == "v0.4.9"

  v2 := core.ParseVersion("1.0.0\nChangelog...")
  // v2 == "1.0.0"
  ```

notes:
  Toma solo el primer campo (separado por whitespace) de la primera linea. No valida formato semver — simplemente extrae el primer token.

code:
  package core

  import "strings"

  // ParseVersion extracts the version tag from the first word of the first line.
  // For example, "v1.4.0 - Description" returns "v1.4.0".
  // Returns "" if the input is empty.
  func ParseVersion(content string) string {
  	line := strings.SplitN(strings.TrimSpace(content), "\n", 2)[0]
  	word := strings.Fields(line)
  	if len(word) == 0 {
  		return ""
  	}
  	return word[0]
  }

params_schema:
  {"params":[{"name":"content","desc":"texto con version en la primera palabra de la primera linea"}],"output":"string con el tag de version (ej: 'v1.4.0'), o vacio"}


id: partial2_go_core
name: partial2
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Partial2[A, B, C any](f func(A, B) C, a A) func(B) C
description: Aplica parcialmente el primer argumento de una funcion de dos parametros.
tags: ["functional","generic","partial"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/partial2.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: af61869abe22c64a6f94e517b5a2b8f30ccc27fa6f66f297c422d0a4cbdba9bf
example:
  ```go
  mul := func(a, b int) int { return a * b }
  double := Partial2(mul, 2)
  result := double(5) // 10
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Partial2 aplica parcialmente el primer argumento de una funcion de dos parametros.
  func Partial2[A, B, C any](f func(A, B) C, a A) func(B) C {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"f","desc":"función de dos argumentos a aplicar parcialmente: (A, B) -\u003e C"},{"name":"a","desc":"valor para fijar el primer argumento"}],"output":"función parcial con primer argumento fijo: B -\u003e C"}


id: partition_go_core
name: partition
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Partition[T any](xs []T, pred func(T) bool) ([]T, []T)
tags: ["slice","partition","functional","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/partition.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a1c695ac10f249c0e76b946867ece956aca4a4923f03c3d7820491e8b8f72cff
description:
  Divide un slice en dos segun un predicado. El primer slice contiene los elementos que cumplen el predicado, el segundo los que no. Se preserva el orden original.

example:
  ```go
  evens, odds := Partition([]int{1, 2, 3, 4, 5}, func(n int) bool { return n%2 == 0 })
  // evens = [2, 4]
  // odds  = [1, 3, 5]
  ```

notes:
  Funcion pura generica. Ambos slices retornados son nuevos (no muta el slice original). Si el slice de entrada esta vacio, retorna dos slices vacios. Los elementos mantienen su orden relativo original en cada particion.

code:
  package core

  // Partition splits xs into two slices: the first contains elements where pred returns true,
  // the second contains elements where pred returns false. Original order is preserved in both.
  func Partition[T any](xs []T, pred func(T) bool) ([]T, []T) {
  	trueSlice := make([]T, 0)
  	falseSlice := make([]T, 0)
  	for _, x := range xs {
  		if pred(x) {
  			trueSlice = append(trueSlice, x)
  		} else {
  			falseSlice = append(falseSlice, x)
  		}
  	}
  	return trueSlice, falseSlice
  }

params_schema:
  {"params":[{"name":"xs","desc":"slice a particionar"},{"name":"pred","desc":"función predicado que retorna true para la primera partición"}],"output":"par de slices: (que cumplen predicado, que no cumplen); ambos preservan orden original"}


id: pipe2_go_core
name: pipe2
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Pipe2[A, B, C any](f1 func(A) B, f2 func(B) C) func(A) C
description: Compone dos funciones de izquierda a derecha. pipe2(f, g)(x) = g(f(x)).
tags: ["functional","composition","generic","pipe"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/pipe2.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: b6242d57e20fecc3c65a38d15f2c3ad4ba0c8bbf8a9e50ac4085ad6ceb27ccad
example:
  ```go
  double := func(x int) int { return x * 2 }
  toString := func(x int) string { return fmt.Sprintf("%d", x) }
  doubleStr := Pipe2(double, toString)
  result := doubleStr(5) // "10"
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Pipe2 compone dos funciones de izquierda a derecha.
  func Pipe2[A, B, C any](f1 func(A) B, f2 func(B) C) func(A) C {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"f1","desc":"primera función a aplicar (izquierda): A -\u003e B"},{"name":"f2","desc":"segunda función a aplicar (derecha): B -\u003e C"}],"output":"función compuesta que aplica f1 después f2, equivalente a f2(f1(x))"}


id: pipe3_go_core
name: pipe3
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Pipe3[A, B, C, D any](f1 func(A) B, f2 func(B) C, f3 func(C) D) func(A) D
description: Compone tres funciones de izquierda a derecha.
tags: ["functional","composition","generic","pipe"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/pipe3.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: f58d9fa16da7bf9b376ca76b49e480e0402f4f53bb1b469761a774de6bbe6f49
example:
  ```go
  add1 := func(x int) int { return x + 1 }
  double := func(x int) int { return x * 2 }
  toString := func(x int) string { return fmt.Sprintf("%d", x) }
  pipeline := Pipe3(add1, double, toString)
  result := pipeline(3) // "8"
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Pipe3 compone tres funciones de izquierda a derecha.
  func Pipe3[A, B, C, D any](f1 func(A) B, f2 func(B) C, f3 func(C) D) func(A) D {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"f1","desc":"primera función: A -\u003e B"},{"name":"f2","desc":"segunda función: B -\u003e C"},{"name":"f3","desc":"tercera función: C -\u003e D"}],"output":"función compuesta que aplica f1, luego f2, luego f3, equivalente a f3(f2(f1(x)))"}


id: pipeline_go_core
name: pipeline
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Pipeline[T any](fns ...func(T) T) func(T) T
tags: ["pipeline","compose","functional","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/pipeline.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7036616dc0ff0ff7494cd495e2941c5d5cde214beb77c2eea904e1c2141af2b6
description:
  Compone funciones T -> T en secuencia de izquierda a derecha. Pipeline(f, g, h)(x) equivale a h(g(f(x))). Sin funciones retorna la identidad.

example:
  ```go
  transform := Pipeline(
      func(s string) string { return strings.TrimSpace(s) },
      func(s string) string { return strings.ToUpper(s) },
      func(s string) string { return "[" + s + "]" },
  )
  transform("  hello  ") // "[HELLO]"
  ```

notes:
  Funcion pura generica. Las funciones se aplican en orden de izquierda a derecha (no es composicion matematica tradicional). Si se pasa un slice vacio de funciones, la funcion retornada actua como identidad.

code:
  package core

  // Pipeline composes a sequence of functions T -> T into a single function.
  // The functions are applied left to right: Pipeline(f, g, h)(x) == h(g(f(x))).
  // Returns the identity function if no functions are provided.
  func Pipeline[T any](fns ...func(T) T) func(T) T {
  	return func(val T) T {
  		for _, fn := range fns {
  			val = fn(val)
  		}
  		return val
  	}
  }

params_schema:
  {"params":[{"name":"fns","desc":"funciones variádicas de tipo T -\u003e T a componer en secuencia"}],"output":"función que aplica todas las funciones de izquierda a derecha; si no hay funciones, retorna la identidad"}


id: reduce_go_core
name: reduce
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U
description: Reduce un slice a un unico valor aplicando una funcion acumuladora de izquierda a derecha.
tags: ["slice","functional","generic","fold"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  sum := Reduce([]int{1, 2, 3, 4}, 0, func(acc, x int) int { return acc + x })
  // sum = 10
  ```

tests: []
file_path: functions/core/reduce.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 93fd077681f7977f5d0900d413780b82a3d01430560858a09bec9bdf637a13f8
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Reduce reduce un slice a un unico valor aplicando una funcion acumuladora.
  func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U {
  	// stub
  	return initial
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice de elementos a reducir"},{"name":"initial","desc":"valor inicial del acumulador"},{"name":"f","desc":"función acumuladora que toma (acumulador, elemento) y retorna nuevo acumulador"}],"output":"valor final del acumulador después de procesar todos los elementos"}


id: rel_or_full_go_core
name: rel_or_full
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func RelOrFull(base, target string) string
tags: ["core","path","relative","filepath","display"]
uses_functions: []
uses_types: []
returns: []
imports: ["path/filepath"]
tests: []
file_path: functions/core/rel_or_full.go
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: a72146d3b0ceefcf831dd553fa317de309d1f85f557a59284322b9d6753c0fb9
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: launcher/core/script_query.go
description:
  Retorna el path de target relativo a base. Si filepath.Rel falla, retorna target sin cambios. Util para mostrar paths cortos en UIs.

example:
  ```go
  rel := core.RelOrFull("/home/user/project", "/home/user/project/src/main.go")
  // rel == "src/main.go"

  full := core.RelOrFull("/home/user", "/other/path")
  // full == "../../other/path"
  ```

notes:
  Wrapper seguro sobre `filepath.Rel` que nunca retorna error. Si los paths estan en volúmenes distintos (Windows), retorna el target original.

code:
  package core

  import "path/filepath"

  // RelOrFull returns target relative to base when possible,
  // otherwise returns target unchanged.
  func RelOrFull(base, target string) string {
  	rel, err := filepath.Rel(base, target)
  	if err != nil {
  		return target
  	}
  	return rel
  }

params_schema:
  {"params":[{"name":"base","desc":"directorio base para calcular la ruta relativa"},{"name":"target","desc":"path absoluto o relativo a convertir"}],"output":"path relativo a base, o target original si la conversion falla"}


id: retry_with_backoff_go_core
name: retry_with_backoff
kind: function
lang: go
domain: core
version: 1.0.0
purity: impure
tags: ["retry","backoff","resilience","generic"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: functions/core/retry_with_backoff.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 99a032242bf569b8693552ae02003deaeb9cb4a5b752b47d999ebbc20636ed30
signature:
  func RetryWithBackoff[T any](fn func() (T, error), maxRetries int, baseDelay time.Duration) (T, error)

description:
  Reintenta una funcion impura con backoff exponencial. El delay entre intento i e i+1 es baseDelay * 2^i. Retorna el primer resultado exitoso o el ultimo error tras agotar los reintentos.

example:
  ```go
  result, err := RetryWithBackoff(func() (string, error) {
      resp, err := http.Get("https://api.example.com/data")
      if err != nil {
          return "", err
      }
      defer resp.Body.Close()
      return "ok", nil
  }, 3, 100*time.Millisecond)
  // Intenta hasta 4 veces (1 inicial + 3 reintentos)
  // Delays: 100ms, 200ms, 400ms
  ```

notes:
  Funcion impura generica que usa time.Sleep para el backoff. El numero total de intentos es maxRetries + 1 (el intento inicial mas los reintentos). Si maxRetries es negativo retorna error inmediatamente.

code:
  package core

  import (
  	"fmt"
  	"time"
  )

  // RetryWithBackoff retries fn up to maxRetries times with exponential backoff.
  // The delay between attempt i and i+1 is baseDelay * 2^i.
  // Returns the first successful result or the last error after all retries are exhausted.
  func RetryWithBackoff[T any](fn func() (T, error), maxRetries int, baseDelay time.Duration) (T, error) {
  	var zero T
  	if maxRetries < 0 {
  		return zero, fmt.Errorf("maxRetries must be >= 0, got %d", maxRetries)
  	}

  	var lastErr error
  	for attempt := 0; attempt <= maxRetries; attempt++ {
  		result, err := fn()
  		if err == nil {
  			return result, nil
  		}
  		lastErr = err

  		if attempt < maxRetries {
  			delay := baseDelay * (1 << uint(attempt))
  			time.Sleep(delay)
  		}
  	}
  	return zero, fmt.Errorf("all %d retries exhausted: %w", maxRetries+1, lastErr)
  }

params_schema:
  {"params":[{"name":"fn","desc":"función impura a reintentar; retorna (resultado, error)"},{"name":"maxRetries","desc":"número máximo de reintentos después del intento inicial"},{"name":"baseDelay","desc":"delay base en time.Duration; cada reintento usa baseDelay * 2^i"}],"output":"primer (resultado, nil) exitoso, o último error si se agotan los reintentos"}


id: rewrite_rule_go_core
name: rewrite_rule
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func RewriteRule(rule, jsonColumn string) string
tags: ["sql","json","sqlite","rewrite","assertion"]
uses_functions: []
uses_types: []
returns: []
imports: ["regexp"]
tests: []
file_path: functions/core/rewrite_rule.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 21665ee1f88a41a44a6158d6977e929977605925c208ed197a3dc2ca7011012d
description:
  Reescribe campos bare en una expresion SQL a llamadas json_extract sobre una columna JSON de SQLite.

example:
  ```go
  out := RewriteRule("price > 100 AND status IS NOT NULL", "metadata")
  // out = "json_extract(metadata, '$.price') > 100 AND json_extract(metadata, '$.status') IS NOT NULL"

  // Si ya tiene json_extract, no modifica nada
  out = RewriteRule("json_extract(data, '$.x') > 0", "data")
  // out = "json_extract(data, '$.x') > 0"
  ```

notes:
  Funcion pura. Preserva keywords SQL y funciones SQLite conocidas. Util para construir queries dinamicas sobre columnas JSON en SQLite sin que el usuario tenga que escribir json_extract manualmente.

code:
  package core

  import (
  	"fmt"
  	"regexp"
  	"strings"
  )

  var rewriteFieldPattern = regexp.MustCompile(`\b([a-zA-Z_][a-zA-Z0-9_]*)\b`)

  var rewriteSQLKeywords = map[string]bool{
  	"AND": true, "OR": true, "NOT": true, "IS": true, "NULL": true,
  	"IN": true, "BETWEEN": true, "LIKE": true, "GLOB": true,
  	"TRUE": true, "FALSE": true, "CASE": true, "WHEN": true,
  	"THEN": true, "ELSE": true, "END": true, "SELECT": true,
  	"FROM": true, "WHERE": true, "AS": true, "CAST": true,
  }

  var rewriteSQLFunctions = map[string]bool{
  	"json_extract": true, "datetime": true, "now": true,
  	"abs": true, "avg": true, "count": true, "max": true, "min": true,
  	"sum": true, "total": true, "length": true, "typeof": true,
  	"coalesce": true, "ifnull": true, "nullif": true,
  	"upper": true, "lower": true, "trim": true, "replace": true,
  	"substr": true, "instr": true, "round": true,
  }

  // RewriteRule transforms a rule expression so that bare field names become
  // json_extract calls on a given JSON column.
  // For example, with column "metadata":
  //
  //	"price > 100 AND status IS NOT NULL"
  //
  // becomes:
  //
  //	"json_extract(metadata, '$.price') > 100 AND json_extract(metadata, '$.status') IS NOT NULL"
  //
  // If the rule already contains json_extract, it is returned as-is.
  // SQL keywords and common SQL functions are preserved.
  func RewriteRule(rule, jsonColumn string) string {
  	if strings.Contains(rule, "json_extract") {
  		return rule
  	}

  	return rewriteFieldPattern.ReplaceAllStringFunc(rule, func(match string) string {
  		upper := strings.ToUpper(match)
  		if rewriteSQLKeywords[upper] {
  			return match
  		}
  		if rewriteSQLFunctions[strings.ToLower(match)] {
  			return match
  		}
  		if match[0] >= '0' && match[0] <= '9' {
  			return match
  		}
  		return fmt.Sprintf("json_extract(%s, '$.%s')", jsonColumn, match)
  	})
  }

params_schema:
  {"params":[{"name":"rule","desc":"expresión SQL con campos bare (sin prefijo) a reescribir como json_extract"},{"name":"jsonColumn","desc":"nombre de la columna JSON donde extraer los valores"}],"output":"expresión SQL reescrita con json_extract para cada campo bare"}


id: split_command_and_arg_go_core
name: split_command_and_arg
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func SplitCommandAndArg(input string) (cmd, arg string, ok bool)
tags: ["core","string","split","command","parse","cli"]
uses_functions: []
uses_types: []
returns: []
imports: ["strings"]
tests: []
file_path: functions/core/split_command_and_arg.go
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 40fdb2156b637a3914391fe315522a851fd4d75d626438e698fa3a47bc366273
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: launcher/core/commands.go
description:
  Separa un string 'comando argumento' en sus dos partes por el primer espacio. Retorna ok=false si no hay espacio o el input esta vacio.

example:
  ```go
  cmd, arg, ok := core.SplitCommandAndArg("cd /home/user")
  // cmd == "cd", arg == "/home/user", ok == true

  _, _, ok2 := core.SplitCommandAndArg("exit")
  // ok2 == false
  ```

notes:
  Solo separa por el primer espacio. El argumento puede contener espacios adicionales. Trims whitespace en ambos extremos del input y de cada parte.

code:
  package core

  import "strings"

  // SplitCommandAndArg splits "cmd arg" into its two parts.
  // Returns (cmd, arg, true) if a space separator is found,
  // or ("", "", false) if the input has no space or is empty.
  func SplitCommandAndArg(input string) (cmd, arg string, ok bool) {
  	trimmed := strings.TrimSpace(input)
  	idx := strings.Index(trimmed, " ")
  	if idx == -1 {
  		return "", "", false
  	}
  	cmd = strings.TrimSpace(trimmed[:idx])
  	arg = strings.TrimSpace(trimmed[idx+1:])
  	if cmd == "" {
  		return "", "", false
  	}
  	return cmd, arg, true
  }

params_schema:
  {"params":[{"name":"input","desc":"string con formato 'comando argumento'"}],"output":"tupla (cmd, arg, ok) donde ok indica si se encontro separador"}


id: take_go_core
name: take
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Take[T any](slice []T, n int) []T
description: Devuelve los primeros n elementos de un slice.
tags: ["slice","functional","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  result := Take([]int{1, 2, 3, 4, 5}, 3)
  // result = [1, 2, 3]
  ```

tests: []
file_path: functions/core/take.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: bd1cdfa21cf1a9661472287d0d9f2142efb2cfc5a02077cc75afa1a5c2145bab
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Take devuelve los primeros n elementos de un slice.
  func Take[T any](slice []T, n int) []T {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice del cual tomar elementos del inicio"},{"name":"n","desc":"cantidad de elementos a tomar desde el inicio"}],"output":"nuevo slice con los primeros n elementos; si n \u003e len(slice), retorna el slice completo"}


id: uncurry2_go_core
name: uncurry2
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Uncurry2[A, B, C any](f func(A) func(B) C) func(A, B) C
description: Transforma una funcion currificada en una funcion normal de dos argumentos.
tags: ["functional","generic","curry"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/core/uncurry2.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 1c7d5939afce813c437f77af7cde50000aa11a8d6fd477c836e4196ab59017b5
example:
  ```go
  curriedAdd := func(a int) func(int) int {
      return func(b int) int { return a + b }
  }
  add := Uncurry2(curriedAdd)
  result := add(3, 5) // 8
  ```

code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Uncurry2 transforma una funcion currificada en una funcion normal de dos argumentos.
  func Uncurry2[A, B, C any](f func(A) func(B) C) func(A, B) C {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"f","desc":"función currificada a descurrificar: A -\u003e (B -\u003e C)"}],"output":"función normal de dos argumentos: (A, B) -\u003e C"}


id: unique_go_core
name: unique
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Unique[T comparable](slice []T) []T
description: Devuelve un slice con elementos unicos preservando el orden original.
tags: ["slice","functional","generic","dedup"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  result := Unique([]int{1, 2, 2, 3, 1, 4})
  // result = [1, 2, 3, 4]
  ```

tests: []
file_path: functions/core/unique.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: f1707d34984eb813d9ce6134e15a9820f86bcc1e2d3d4ec69176781ef1d51fb6
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Unique devuelve un slice con elementos unicos preservando el orden original.
  func Unique[T comparable](slice []T) []T {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"slice","desc":"slice del cual eliminar duplicados"}],"output":"nuevo slice con elementos únicos preservando orden de primera aparición"}


id: validate_struct_fields_go_core
name: validate_struct_fields
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func ValidateStructFields(data map[string]any, rules map[string]string) (bool, []string)
tags: ["validation","map","rules","pure","core","operations"]
uses_functions: []
uses_types: []
returns: []
imports: ["fmt","regexp","strconv","strings"]
test_file_path: functions/core/validate_struct_fields_test.go
file_path: functions/core/validate_struct_fields.go
created_at: 2026-04-05T11:24:27Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f43dc89134af08676ff273da3950eb01a797e1d988d0cb2b1cafd8a1ca33964c
description:
  Valida campos de un map[string]any contra reglas declarativas tipo 'required,min=1,max=100,type=string'. Soporta required, type, min/max, minlen/maxlen, oneof, pattern. Pensado para validar metadata de entities en operations.db o resultados de queries sin definir structs Go. Acumula todos los errores.

example:
  ```go
  data := map[string]any{
      "name":   "Alice",
      "age":    30,
      "status": "active",
      "email":  "alice@example.com",
  }

  rules := map[string]string{
      "name":   "required,type=string,minlen=2,maxlen=100",
      "age":    "required,type=int,min=0,max=150",
      "status": "required,oneof=active|inactive|pending",
      "email":  `required,type=string,pattern=^[^@]+@[^@]+$`,
  }

  valid, errs := ValidateStructFields(data, rules)
  // valid = true, errs = []

  data2 := map[string]any{"name": "A", "age": 200, "status": "deleted"}
  valid2, errs2 := ValidateStructFields(data2, rules)
  // valid2 = false
  // errs2 = [
  //   "name: length 1 < minlen 2",
  //   "age: 200 > max 150",
  //   "status: value \"deleted\" not in oneof [active|inactive|pending]",
  //   "email: required field missing",
  // ]
  ```

tests:
  ["campo required presente y ausente","type validation string como int falla","numeric ranges","string lengths","oneof validation","pattern matching","multiples reglas combinadas","map vacio con reglas required"]

notes:
  Funcion pura. Solo usa stdlib (fmt, regexp, strconv, strings). Las reglas se evaluan en orden y se acumulan todos los errores. Si `required` falla, se omiten las reglas restantes de ese campo para evitar falsos positivos. Tipos Go aceptados para type=int: int, int8..int64, uint..uint64. Tipo float acepta enteros tambien. Pattern compila el regex en cada llamada — para uso intensivo cachear los regexp compilados fuera.

code:
  package core

  import (
  	"fmt"
  	"regexp"
  	"strconv"
  	"strings"
  )

  // ValidateStructFields validates fields of a map against declarative rules.
  // Each rule is a comma-separated string like "required,type=string,min=1,max=100".
  //
  // Supported rules:
  //   - required       — field must exist and not be nil or ""
  //   - type=string|int|float|bool — validate underlying Go type
  //   - min=N, max=N   — for numeric values
  //   - minlen=N, maxlen=N — for string values
  //   - oneof=a|b|c    — value must be one of the listed options
  //   - pattern=regex  — for string values
  //
  // Returns (valid, errors). Errors accumulate — all fields are checked.
  func ValidateStructFields(data map[string]any, rules map[string]string) (bool, []string) {
  	var errs []string

  	for field, ruleStr := range rules {
  		parts := strings.Split(ruleStr, ",")
  		for _, part := range parts {
  			part = strings.TrimSpace(part)
  			if part == "" {
  				continue
  			}

  			if err := applyRule(data, field, part); err != "" {
  				errs = append(errs, err)
  				// stop further checks on this field if required failed
  				if part == "required" {
  					break
  				}
  			}
  		}
  	}

  	return len(errs) == 0, errs
  }

  // applyRule applies a single rule to a field and returns an error string or "".
  func applyRule(data map[string]any, field, rule string) string {
  	switch {
  	case rule == "required":
  		val, ok := data[field]
  		if !ok || val == nil {
  			return fmt.Sprintf("%s: required field missing", field)
  		}
  		if s, ok := val.(string); ok && s == "" {
  			return fmt.Sprintf("%s: required field is empty string", field)
  		}
  		return ""

  	case strings.HasPrefix(rule, "type="):
  		expectedType := rule[len("type="):]
  		val, ok := data[field]
  		if !ok || val == nil {
  			return "" // absence handled by required
  		}
  		return checkType(field, val, expectedType)

  	case strings.HasPrefix(rule, "min="):
  		n, err := strconv.ParseFloat(rule[len("min="):], 64)
  		if err != nil {
  			return fmt.Sprintf("%s: invalid rule min value: %s", field, rule)
  		}
  		val, ok := data[field]
  		if !ok || val == nil {
  			return ""
  		}
  		f, ok := toFloat(val)
  		if !ok {
  			return fmt.Sprintf("%s: cannot apply min to non-numeric value", field)
  		}
  		if f < n {
  			return fmt.Sprintf("%s: %v < min %v", field, val, n)
  		}
  		return ""

  	case strings.HasPrefix(rule, "max="):
  		n, err := strconv.ParseFloat(rule[len("max="):], 64)
  		if err != nil {
  			return fmt.Sprintf("%s: invalid rule max value: %s", field, rule)
  		}
  		val, ok := data[field]
  		if !ok || val == nil {
  			return ""
  		}
  		f, ok := toFloat(val)
  		if !ok {
  			return fmt.Sprintf("%s: cannot apply max to non-numeric value", field)
  		}
  		if f > n {
  			return fmt.Sprintf("%s: %v > max %v", field, val, n)
  		}
  		return ""

  	case strings.HasPrefix(rule, "minlen="):
  		n, err := strconv.Atoi(rule[len("minlen="):])
  		if err != nil {
  			return fmt.Sprintf("%s: invalid rule minlen value: %s", field, rule)
  		}
  		val, ok := data[field]
  		if !ok || val == nil {
  			return ""
  		}
  		s, ok := val.(string)
  		if !ok {
  			return fmt.Sprintf("%s: cannot apply minlen to non-string value", field)
  		}
  		if len(s) < n {
  			return fmt.Sprintf("%s: length %d < minlen %d", field, len(s), n)
  		}
  		return ""

  	case strings.HasPrefix(rule, "maxlen="):
  		n, err := strconv.Atoi(rule[len("maxlen="):])
  		if err != nil {
  			return fmt.Sprintf("%s: invalid rule maxlen value: %s", field, rule)
  		}
  		val, ok := data[field]
  		if !ok || val == nil {
  			return ""
  		}
  		s, ok := val.(string)
  		if !ok {
  			return fmt.Sprintf("%s: cannot apply maxlen to non-string value", field)
  		}
  		if len(s) > n {
  			return fmt.Sprintf("%s: length %d > maxlen %d", field, len(s), n)
  		}
  		return ""

  	case strings.HasPrefix(rule, "oneof="):
  		options := strings.Split(rule[len("oneof="):], "|")
  		val, ok := data[field]
  		if !ok || val == nil {
  			return ""
  		}
  		sval := fmt.Sprintf("%v", val)
  		for _, opt := range options {
  			if sval == opt {
  				return ""
  			}
  		}
  		return fmt.Sprintf("%s: value %q not in oneof [%s]", field, sval, rule[len("oneof="):])

  	case strings.HasPrefix(rule, "pattern="):
  		pat := rule[len("pattern="):]
  		val, ok := data[field]
  		if !ok || val == nil {
  			return ""
  		}
  		s, ok := val.(string)
  		if !ok {
  			return fmt.Sprintf("%s: cannot apply pattern to non-string value", field)
  		}
  		re, err := regexp.Compile(pat)
  		if err != nil {
  			return fmt.Sprintf("%s: invalid pattern %q: %v", field, pat, err)
  		}
  		if !re.MatchString(s) {
  			return fmt.Sprintf("%s: value %q does not match pattern %q", field, s, pat)
  		}
  		return ""

  	default:
  		return fmt.Sprintf("%s: unknown rule %q", field, rule)
  	}
  }

  func checkType(field string, val any, expected string) string {
  	var ok bool
  	switch expected {
  	case "string":
  		_, ok = val.(string)
  	case "int":
  		switch val.(type) {
  		case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
  			ok = true
  		}
  	case "float":
  		switch val.(type) {
  		case float32, float64:
  			ok = true
  		case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
  			ok = true // integers are valid floats
  		}
  	case "bool":
  		_, ok = val.(bool)
  	default:
  		return fmt.Sprintf("%s: unknown type rule %q", field, expected)
  	}
  	if !ok {
  		return fmt.Sprintf("%s: expected type %s, got %T", field, expected, val)
  	}
  	return ""
  }

  func toFloat(val any) (float64, bool) {
  	switch v := val.(type) {
  	case int:
  		return float64(v), true
  	case int8:
  		return float64(v), true
  	case int16:
  		return float64(v), true
  	case int32:
  		return float64(v), true
  	case int64:
  		return float64(v), true
  	case uint:
  		return float64(v), true
  	case uint8:
  		return float64(v), true
  	case uint16:
  		return float64(v), true
  	case uint32:
  		return float64(v), true
  	case uint64:
  		return float64(v), true
  	case float32:
  		return float64(v), true
  	case float64:
  		return v, true
  	}
  	return 0, false
  }

params_schema:
  {"params":[{"name":"data","desc":"map[string]any con los campos a validar"},{"name":"rules","desc":"map[string]string donde cada clave es un campo y el valor es una lista de reglas (required, type, min/max, minlen/maxlen, oneof, pattern)"}],"output":"tupla (válido: bool, errores: []string); si válido es true, errores está vacío"}


id: zip_go_core
name: zip
kind: function
lang: go
domain: core
version: 1.0.0
purity: pure
signature: func Zip[T, U any](a []T, b []U) []Pair[T, U]
description: Combina dos slices en un slice de pares elemento a elemento.
tags: ["slice","functional","generic","pair"]
uses_functions: []
uses_types: ["pair_go_core"]
returns: ["pair_go_core"]
imports: []
example:
  ```go
  pairs := Zip([]string{"a", "b"}, []int{1, 2})
  // pairs = [{a 1}, {b 2}]
  ```

tests: []
file_path: functions/core/zip.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Funcion pura generica. Implementacion en devfactory/core.
content_hash: 380a81ef16628b20c65dbd8104fa4b5047355e8352a5dcc8f924c520b786582c
code:
  package core

  // Implementation: github.com/lucasdataproyects/devfactory/core

  // Zip combina dos slices en un slice de pares elemento a elemento.
  func Zip[T, U any](a []T, b []U) []Pair[T, U] {
  	// stub
  	return nil
  }

params_schema:
  {"params":[{"name":"a","desc":"primer slice"},{"name":"b","desc":"segundo slice"}],"output":"slice de pares (Pair[T, U]) emparejando elementos por índice; la longitud es min(len(a), len(b))"}


id: detect_sql_injection_go_cybersecurity
name: detect_sql_injection
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func DetectSQLInjection(input string) (isThreat bool, pattern string)
tags: ["cybersecurity","sqli","detection","security"]
uses_functions: []
uses_types: []
returns: []
imports: ["regexp","strings"]
tests: []
file_path: functions/cybersecurity/detect_sql_injection.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 532b907cfd169946aa32a259b12d4db396d423113457404929417c56c557c38a
description:
  Analiza un input en busca de patrones heuristicos de inyeccion SQL y devuelve si se detecto amenaza y el patron encontrado.

code:
  package cybersecurity

  import (
  	"regexp"
  	"strings"
  )

  var sqliPatterns = []struct {
  	name string
  	re   *regexp.Regexp
  }{
  	{"union_select", regexp.MustCompile(`(?i)\bunion\s+(all\s+)?select\b`)},
  	{"or_1_eq_1", regexp.MustCompile(`(?i)\bor\s+1\s*=\s*1`)},
  	{"comment_injection", regexp.MustCompile(`(--|#|/\*)\s*$`)},
  	{"single_quote_or", regexp.MustCompile(`(?i)'\s*(or|and)\s+'`)},
  	{"drop_table", regexp.MustCompile(`(?i)\bdrop\s+(table|database)\b`)},
  	{"sleep_benchmark", regexp.MustCompile(`(?i)\b(sleep|benchmark)\s*\(`)},
  	{"exec_xp", regexp.MustCompile(`(?i)\b(exec|xp_)\w*`)},
  	{"tautology", regexp.MustCompile(`(?i)\bor\s+['"]?\w+['"]?\s*=\s*['"]?\w+['"]?`)},
  	{"stacked_query", regexp.MustCompile(`;\s*(select|insert|update|delete|drop|alter)\b`)},
  }

  // DetectSQLInjection analiza un input en busca de patrones heuristicos de inyeccion SQL.
  // Devuelve si se detecto una amenaza y el nombre del patron encontrado.
  func DetectSQLInjection(input string) (isThreat bool, pattern string) {
  	normalized := strings.TrimSpace(input)
  	for _, p := range sqliPatterns {
  		if p.re.MatchString(normalized) {
  			return true, p.name
  		}
  	}
  	return false, ""
  }

params_schema:
  {"params":[{"name":"input","desc":"string a analizar en busca de patrones de SQL injection"}],"output":"tupla (isThreat, pattern): booleano indicando si se detectó amenaza, y nombre del patron encontrado"}


id: entropy_shannon_go_cybersecurity
name: entropy_shannon
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func EntropyShannon(data []byte) float64
description: Calcula la entropia de Shannon de un slice de bytes. Retorna un valor entre 0 y 8 bits por byte.
tags: ["cybersecurity","entropy","shannon","analysis"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
tests: []
file_path: functions/cybersecurity/entropy_shannon.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 76a35f35ba05d86b42e08a6607a12c62ff2d969242aa1be1bfc22e8c3bdc479c
code:
  package cybersecurity

  import "math"

  // EntropyShannon calcula la entropia de Shannon de los datos proporcionados.
  // Devuelve un valor entre 0 (datos uniformes) y 8 (datos completamente aleatorios para bytes).
  func EntropyShannon(data []byte) float64 {
  	if len(data) == 0 {
  		return 0
  	}

  	var freq [256]float64
  	for _, b := range data {
  		freq[b]++
  	}

  	n := float64(len(data))
  	entropy := 0.0
  	for _, f := range freq {
  		if f > 0 {
  			p := f / n
  			entropy -= p * math.Log2(p)
  		}
  	}
  	return entropy
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de bytes cuya entropia se desea calcular"}],"output":"valor de entropia Shannon entre 0 y 8 bits por byte"}


id: extract_urls_go_cybersecurity
name: extract_urls
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func ExtractURLs(text string) []string
description: Extrae todas las URLs HTTP/HTTPS de un texto usando expresiones regulares.
tags: ["cybersecurity","extract","url","parse"]
uses_functions: []
uses_types: []
returns: []
imports: ["regexp"]
tests: []
file_path: functions/cybersecurity/extract_urls.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: df4bb3817fdccc0e63232fd9784ca33e29327288439544a83bed1407c7f0bd1c
code:
  package cybersecurity

  import "regexp"

  var urlRegex = regexp.MustCompile(`https?://[^\s<>"'` + "`" + `\)\]\}]+`)

  // ExtractURLs extrae todas las URLs (http/https) encontradas en el texto proporcionado.
  func ExtractURLs(text string) []string {
  	matches := urlRegex.FindAllString(text, -1)
  	if matches == nil {
  		return []string{}
  	}
  	return matches
  }

params_schema:
  {"params":[{"name":"text","desc":"string de texto del cual extraer URLs HTTP/HTTPS"}],"output":"slice de strings con todas las URLs encontradas"}


id: fetch_http_headers_go_cybersecurity
name: fetch_http_headers
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: impure
signature: func FetchHTTPHeaders(url string) (map[string][]string, error)
description: Realiza una solicitud HTTP HEAD a una URL y devuelve los headers de la respuesta.
tags: ["cybersecurity","io","http","headers"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","net/http","time"]
tests: []
file_path: functions/cybersecurity/fetch_http_headers.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f117d9e7cbcb4a42acf982c6f7c314b74dea358e20532ccd0b3c3e22bb064016
code:
  package cybersecurity

  import (
  	"fmt"
  	"net/http"
  	"time"
  )

  // FetchHTTPHeaders realiza una solicitud HTTP HEAD a la URL y devuelve los headers de respuesta.
  func FetchHTTPHeaders(url string) (map[string][]string, error) {
  	client := &http.Client{
  		Timeout: 10 * time.Second,
  	}

  	resp, err := client.Head(url)
  	if err != nil {
  		return nil, fmt.Errorf("error realizando solicitud HEAD a %s: %w", url, err)
  	}
  	defer resp.Body.Close()

  	headers := make(map[string][]string, len(resp.Header))
  	for k, v := range resp.Header {
  		headers[k] = v
  	}

  	return headers, nil
  }

params_schema:
  {"params":[{"name":"url","desc":"URL destino para realizar la solicitud HTTP HEAD"}],"output":"mapa de headers de la respuesta (map[string][]string) o error si la solicitud falla"}


id: hash_md5_go_cybersecurity
name: hash_md5
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func HashMD5(data []byte) string
description: Calcula el hash MD5 de un slice de bytes y devuelve el resultado como string hexadecimal.
tags: ["cybersecurity","hash","md5","crypto"]
uses_functions: []
uses_types: []
returns: []
imports: ["crypto/md5","encoding/hex"]
tests: []
file_path: functions/cybersecurity/hash_md5.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5e876b04c5b57c54806897043379069f768e960ff3b6811610b51de964a02f9c
code:
  package cybersecurity

  import (
  	"crypto/md5"
  	"encoding/hex"
  )

  // HashMD5 calcula el hash MD5 de los datos proporcionados y devuelve el resultado como string hexadecimal.
  func HashMD5(data []byte) string {
  	h := md5.Sum(data)
  	return hex.EncodeToString(h[:])
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de bytes a ser hasheado"}],"output":"string hexadecimal del hash MD5 (32 caracteres)"}


id: hash_sha256_go_cybersecurity
name: hash_sha256
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func HashSHA256(data []byte) string
description: Calcula el hash SHA-256 de un slice de bytes y devuelve el resultado como string hexadecimal.
tags: ["cybersecurity","hash","sha256","crypto"]
uses_functions: []
uses_types: []
returns: []
imports: ["crypto/sha256","encoding/hex"]
tests: []
file_path: functions/cybersecurity/hash_sha256.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 8db06484fe6e8e826483b7e5eb4a340ad693c58af30eba438aa88828310bd696
code:
  package cybersecurity

  import (
  	"crypto/sha256"
  	"encoding/hex"
  )

  // HashSHA256 calcula el hash SHA-256 de los datos proporcionados y devuelve el resultado como string hexadecimal.
  func HashSHA256(data []byte) string {
  	h := sha256.Sum256(data)
  	return hex.EncodeToString(h[:])
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de bytes a ser hasheado"}],"output":"string hexadecimal del hash SHA-256 (64 caracteres)"}


id: ip_in_range_go_cybersecurity
name: ip_in_range
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func IPInRange(ip, cidr string) bool
description: Verifica si una direccion IP se encuentra dentro de un rango CIDR dado.
tags: ["cybersecurity","network","cidr","check"]
uses_functions: []
uses_types: []
returns: []
imports: ["net"]
tests: []
file_path: functions/cybersecurity/ip_in_range.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 66ca1898f40bd35d8f5a716c0fd330f69d1adf67221fb372ebc9422f3f76b15c
code:
  package cybersecurity

  import "net"

  // IPInRange verifica si una direccion IP esta dentro de un rango CIDR dado.
  func IPInRange(ip, cidr string) bool {
  	parsedIP := net.ParseIP(ip)
  	if parsedIP == nil {
  		return false
  	}
  	_, ipNet, err := net.ParseCIDR(cidr)
  	if err != nil {
  		return false
  	}
  	return ipNet.Contains(parsedIP)
  }

params_schema:
  {"params":[{"name":"ip","desc":"direccion IP en formato string (ej: 192.168.1.1)"},{"name":"cidr","desc":"rango CIDR en formato string (ej: 192.168.0.0/24)"}],"output":"booleano indicando si la IP se encuentra dentro del rango CIDR"}


id: is_base64_go_cybersecurity
name: is_base64
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func IsBase64(s string) bool
description: Valida si un string es una cadena base64 valida segun el encoding estandar.
tags: ["cybersecurity","validation","base64","format"]
uses_functions: []
uses_types: []
returns: []
imports: ["encoding/base64"]
tests: []
file_path: functions/cybersecurity/is_base64.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7787e0555fabc6c4de6a6602b5a1e1b9205e7f2cb010f258455c51c3317bc3e5
code:
  package cybersecurity

  import "encoding/base64"

  // IsBase64 verifica si el string proporcionado es una cadena base64 valida (standard encoding).
  func IsBase64(s string) bool {
  	if len(s) == 0 {
  		return false
  	}
  	_, err := base64.StdEncoding.DecodeString(s)
  	return err == nil
  }

params_schema:
  {"params":[{"name":"s","desc":"string a validar como base64"}],"output":"booleano indicando si el string es base64 valido segun el encoding estandar"}


id: is_hex_go_cybersecurity
name: is_hex
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func IsHex(s string) bool
description: Valida si un string es una cadena hexadecimal valida (longitud par, caracteres 0-9 a-f A-F).
tags: ["cybersecurity","validation","hex","format"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/cybersecurity/is_hex.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c3911973e44f4591ef15a3feb8425ac4844020a83c3a012a0a2e912645ca8637
code:
  package cybersecurity

  // IsHex verifica si el string proporcionado es una cadena hexadecimal valida.
  // Requiere longitud par y que todos los caracteres sean digitos hex (0-9, a-f, A-F).
  func IsHex(s string) bool {
  	if len(s) == 0 || len(s)%2 != 0 {
  		return false
  	}
  	for _, c := range s {
  		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
  			return false
  		}
  	}
  	return true
  }

params_schema:
  {"params":[{"name":"s","desc":"string a validar como hexadecimal"}],"output":"booleano indicando si el string es hexadecimal valido (caracteres 0-9 a-f A-F con longitud par)"}


id: jaccard_similarity_go_cybersecurity
name: jaccard_similarity
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func JaccardSimilarity(a, b []string) float64
description: Calcula la similitud de Jaccard entre dos conjuntos de tokens. Retorna un valor entre 0.0 y 1.0.
tags: ["cybersecurity","similarity","jaccard","tokens"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/cybersecurity/jaccard_similarity.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 4421678aea40f7e9d18e0af96d1d4606e3c6a67e223fc01fbd343aca13127706
code:
  package cybersecurity

  // JaccardSimilarity calcula la similitud de Jaccard entre dos conjuntos de tokens.
  // Devuelve un valor entre 0.0 (sin interseccion) y 1.0 (conjuntos identicos).
  func JaccardSimilarity(a, b []string) float64 {
  	if len(a) == 0 && len(b) == 0 {
  		return 1.0
  	}
  	if len(a) == 0 || len(b) == 0 {
  		return 0.0
  	}

  	setA := make(map[string]struct{}, len(a))
  	for _, s := range a {
  		setA[s] = struct{}{}
  	}

  	setB := make(map[string]struct{}, len(b))
  	for _, s := range b {
  		setB[s] = struct{}{}
  	}

  	intersection := 0
  	for k := range setA {
  		if _, ok := setB[k]; ok {
  			intersection++
  		}
  	}

  	// Union = |A| + |B| - |A intersect B| (usando conjuntos sin duplicados)
  	union := len(setA) + len(setB) - intersection
  	if union == 0 {
  		return 0.0
  	}

  	return float64(intersection) / float64(union)
  }

params_schema:
  {"params":[{"name":"a","desc":"primer slice de strings para comparacion"},{"name":"b","desc":"segundo slice de strings para comparacion"}],"output":"coeficiente de similitud de Jaccard entre 0.0 y 1.0"}


id: levenshtein_distance_go_cybersecurity
name: levenshtein_distance
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func LevenshteinDistance(a, b string) int
tags: ["cybersecurity","string","distance","typosquatting"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/cybersecurity/levenshtein_distance.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b99117d8425182fd779fefd513f0b8e4690cacf28c56037f1f7400b6da85d665
description:
  Calcula la distancia de edicion de Levenshtein entre dos strings. Util para deteccion de typosquatting.

code:
  package cybersecurity

  // LevenshteinDistance calcula la distancia de edicion (Levenshtein) entre dos strings.
  func LevenshteinDistance(a, b string) int {
  	ra := []rune(a)
  	rb := []rune(b)
  	la := len(ra)
  	lb := len(rb)

  	if la == 0 {
  		return lb
  	}
  	if lb == 0 {
  		return la
  	}

  	// Usar solo dos filas para optimizar memoria
  	prev := make([]int, lb+1)
  	curr := make([]int, lb+1)

  	for j := 0; j <= lb; j++ {
  		prev[j] = j
  	}

  	for i := 1; i <= la; i++ {
  		curr[0] = i
  		for j := 1; j <= lb; j++ {
  			cost := 1
  			if ra[i-1] == rb[j-1] {
  				cost = 0
  			}
  			del := prev[j] + 1
  			ins := curr[j-1] + 1
  			sub := prev[j-1] + cost

  			m := del
  			if ins < m {
  				m = ins
  			}
  			if sub < m {
  				m = sub
  			}
  			curr[j] = m
  		}
  		prev, curr = curr, prev
  	}

  	return prev[lb]
  }

params_schema:
  {"params":[{"name":"a","desc":"primer string para comparacion de distancia de edicion"},{"name":"b","desc":"segundo string para comparacion de distancia de edicion"}],"output":"entero con la distancia de Levenshtein (minimo numero de ediciones necesarias)"}


id: lookup_whois_go_cybersecurity
name: lookup_whois
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: impure
signature: func LookupWhois(domain string) (string, error)
description: Realiza una consulta WHOIS para un dominio conectandose al servidor whois.iana.org por TCP.
tags: ["cybersecurity","io","whois","recon"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","io","net","strings","time"]
tests: []
file_path: functions/cybersecurity/lookup_whois.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 268871acfe3e37754a54f987788c7ad68511aa9ba93c22bb3414b6801cfd6de5
code:
  package cybersecurity

  import (
  	"fmt"
  	"io"
  	"net"
  	"strings"
  	"time"
  )

  // LookupWhois realiza una consulta WHOIS para el dominio proporcionado conectandose al servidor whois.iana.org.
  func LookupWhois(domain string) (string, error) {
  	conn, err := net.DialTimeout("tcp", "whois.iana.org:43", 10*time.Second)
  	if err != nil {
  		return "", fmt.Errorf("error conectando al servidor WHOIS: %w", err)
  	}
  	defer conn.Close()

  	_ = conn.SetDeadline(time.Now().Add(10 * time.Second))

  	_, err = fmt.Fprintf(conn, "%s\r\n", domain)
  	if err != nil {
  		return "", fmt.Errorf("error enviando consulta WHOIS: %w", err)
  	}

  	var sb strings.Builder
  	_, err = io.Copy(&sb, conn)
  	if err != nil {
  		return "", fmt.Errorf("error leyendo respuesta WHOIS: %w", err)
  	}

  	return sb.String(), nil
  }

params_schema:
  {"params":[{"name":"domain","desc":"nombre de dominio para el cual realizar la consulta WHOIS"}],"output":"string con la respuesta WHOIS del servidor, o error si la consulta falla"}


id: normalize_url_go_cybersecurity
name: normalize_url
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func NormalizeURL(rawURL string) string
tags: ["cybersecurity","url","normalize","sanitize"]
uses_functions: []
uses_types: []
returns: []
imports: ["net/url","strings"]
tests: []
file_path: functions/cybersecurity/normalize_url.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7791223434f2c0f5006fa614ac6a9b5dabc5485ba00669b36669207236bddd31
description:
  Normaliza una URL: convierte el host a minusculas, elimina fragmentos y remueve parametros de tracking comunes.

code:
  package cybersecurity

  import (
  	"net/url"
  	"strings"
  )

  // trackingParams lista de parametros de tracking comunes a eliminar.
  var trackingParams = map[string]bool{
  	"utm_source":   true,
  	"utm_medium":   true,
  	"utm_campaign": true,
  	"utm_term":     true,
  	"utm_content":  true,
  	"fbclid":       true,
  	"gclid":        true,
  	"ref":          true,
  	"mc_cid":       true,
  	"mc_eid":       true,
  }

  // NormalizeURL normaliza una URL: convierte el host a minusculas, elimina fragmentos
  // y remueve parametros de tracking comunes.
  func NormalizeURL(rawURL string) string {
  	u, err := url.Parse(rawURL)
  	if err != nil {
  		return rawURL
  	}

  	// Lowercase host
  	u.Host = strings.ToLower(u.Host)

  	// Eliminar fragmento
  	u.Fragment = ""

  	// Eliminar parametros de tracking
  	q := u.Query()
  	for param := range trackingParams {
  		q.Del(param)
  	}
  	u.RawQuery = q.Encode()

  	return u.String()
  }

params_schema:
  {"params":[{"name":"rawURL","desc":"URL en formato string a normalizar"}],"output":"URL normalizada con host en minusculas, sin fragmentos ni parametros de tracking"}


id: parse_ip_cidr_go_cybersecurity
name: parse_ip_cidr
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: pure
signature: func ParseIPCIDR(cidr string) (network string, broadcast string, hosts int, err error)
tags: ["cybersecurity","network","cidr","parse"]
uses_functions: []
uses_types: []
returns: []
imports: ["encoding/binary","fmt","net"]
tests: []
file_path: functions/cybersecurity/parse_ip_cidr.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 76c5de70dac5b7422542f26771a7869122ddfacdc0407ceb6f189817baf0cd33
description:
  Parsea una notacion CIDR IPv4 y devuelve la direccion de red, broadcast y cantidad de hosts usables.

code:
  package cybersecurity

  import (
  	"encoding/binary"
  	"fmt"
  	"net"
  )

  // ParseIPCIDR parsea una notacion CIDR y devuelve la direccion de red, broadcast, cantidad de hosts y error.
  func ParseIPCIDR(cidr string) (network string, broadcast string, hosts int, err error) {
  	ip, ipNet, err := net.ParseCIDR(cidr)
  	if err != nil {
  		return "", "", 0, fmt.Errorf("CIDR invalido: %w", err)
  	}

  	// Solo soportamos IPv4
  	ip4 := ip.To4()
  	if ip4 == nil {
  		return "", "", 0, fmt.Errorf("solo se soporta IPv4")
  	}

  	mask := ipNet.Mask
  	networkIP := ipNet.IP.To4()
  	network = networkIP.String()

  	// Calcular broadcast
  	broadcastIP := make(net.IP, 4)
  	for i := 0; i < 4; i++ {
  		broadcastIP[i] = networkIP[i] | ^mask[i]
  	}
  	broadcast = broadcastIP.String()

  	// Calcular hosts usables
  	netInt := binary.BigEndian.Uint32(networkIP)
  	bcastInt := binary.BigEndian.Uint32(broadcastIP)
  	total := int(bcastInt - netInt + 1)
  	if total > 2 {
  		hosts = total - 2 // excluir network y broadcast
  	} else {
  		hosts = total // /31 o /32
  	}

  	return network, broadcast, hosts, nil
  }

params_schema:
  {"params":[{"name":"cidr","desc":"notacion CIDR IPv4 a parsear (ej: 192.168.0.0/24)"}],"output":"tupla (network, broadcast, hosts, err) con direccion de red, broadcast, cantidad de hosts usables, y error si aplica"}


id: resolve_dns_go_cybersecurity
name: resolve_dns
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: impure
signature: func ResolveDNS(host string) ([]string, error)
description: Resuelve un hostname a sus direcciones IP usando el resolver DNS del sistema.
tags: ["cybersecurity","io","dns","resolve"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","net"]
tests: []
file_path: functions/cybersecurity/resolve_dns.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 0a1df757483164114dc46c1f7af6db0c9bfb6d29c13a02d74ee5612317a41daa
code:
  package cybersecurity

  import (
  	"fmt"
  	"net"
  )

  // ResolveDNS resuelve un hostname a sus direcciones IP usando el resolver del sistema.
  func ResolveDNS(host string) ([]string, error) {
  	ips, err := net.LookupHost(host)
  	if err != nil {
  		return nil, fmt.Errorf("error resolviendo DNS para %s: %w", host, err)
  	}
  	return ips, nil
  }

params_schema:
  {"params":[{"name":"host","desc":"hostname a resolver"}],"output":"slice de strings con direcciones IP resueltas, o error si la resolucion falla"}


id: scan_port_tcp_go_cybersecurity
name: scan_port_tcp
kind: function
lang: go
domain: cybersecurity
version: 1.0.0
purity: impure
signature: func ScanPortTCP(host string, port int, timeoutMs int) (status string, banner string, err error)
tags: ["cybersecurity","io","port","scan"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","net","time"]
tests: []
file_path: functions/cybersecurity/scan_port_tcp.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a063518adf1c5eb4166588045d79c30eeca54ff83cef420d4de1d43ed1959f4d
description:
  Escanea un puerto TCP en un host dado. Devuelve el estado (open/closed/filtered) y un banner si esta abierto.

code:
  package cybersecurity

  import (
  	"fmt"
  	"net"
  	"time"
  )

  // ScanPortTCP intenta conectarse a un puerto TCP y devuelve el estado ("open", "closed", "filtered"),
  // un banner si el puerto esta abierto, y un posible error.
  func ScanPortTCP(host string, port int, timeoutMs int) (status string, banner string, err error) {
  	address := fmt.Sprintf("%s:%d", host, port)
  	timeout := time.Duration(timeoutMs) * time.Millisecond

  	conn, err := net.DialTimeout("tcp", address, timeout)
  	if err != nil {
  		// Distinguir entre conexion rechazada (closed) y timeout (filtered)
  		if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
  			return "filtered", "", nil
  		}
  		return "closed", "", nil
  	}
  	defer conn.Close()

  	// Intentar leer un banner con timeout corto
  	_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
  	buf := make([]byte, 1024)
  	n, _ := conn.Read(buf)
  	if n > 0 {
  		banner = string(buf[:n])
  	}

  	return "open", banner, nil
  }

params_schema:
  {"params":[{"name":"host","desc":"hostname o direccion IP del host a escanear"},{"name":"port","desc":"numero de puerto TCP a escanear"},{"name":"timeoutMs","desc":"timeout en milisegundos para la conexion"}],"output":"tupla (status, banner, err) con estado del puerto (open/closed/filtered), banner si esta abierto, y error si aplica"}


id: autocorrelation_go_datascience
name: autocorrelation
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func Autocorrelation(data []float64, lag int) float64
tags: ["datascience","statistics","autocorrelation","timeseries"]
uses_functions: ["pearson_go_datascience"]
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/datascience/autocorrelation.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d168a430fa0b7ccf46e59548a135431893fc744cc753fa0c09d839a9e8c237c5
description:
  Calcula la autocorrelación de una serie temporal con un desfase (lag) dado, usando correlación de Pearson.

code:
  package datascience

  // Autocorrelation calcula la autocorrelación de data con el desfase (lag) dado.
  // Usa la correlación de Pearson entre data[0:n-lag] y data[lag:n].
  // Si lag es inválido, retorna 0.
  func Autocorrelation(data []float64, lag int) float64 {
  	n := len(data)
  	if lag < 0 || lag >= n {
  		return 0
  	}
  	return Pearson(data[:n-lag], data[lag:])
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores de serie temporal (ej: [100, 102, 101, 103, ...])"},{"name":"lag","desc":"desfase en períodos (ej: 1 para correlación con el período anterior, 12 para correlación anual en datos mensuales)"}],"output":"coeficiente de correlación de Pearson entre data y su versión desfasada, rango [-1, 1]"}


id: clip_go_datascience
name: clip
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func Clip(data []float64, min, max float64) []float64
description: Recorta cada valor del slice para que quede dentro del rango [min, max].
tags: ["datascience","clamp","clip","range"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/datascience/clip.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d160877ac9ce0f6c3085c5356fda7a123e265a382d2ff9bb90488531382a103b
code:
  package datascience

  // Clip recorta cada valor del slice para que quede dentro del rango [min, max].
  func Clip(data []float64, min, max float64) []float64 {
  	result := make([]float64, len(data))
  	for i, v := range data {
  		switch {
  		case v < min:
  			result[i] = min
  		case v > max:
  			result[i] = max
  		default:
  			result[i] = v
  		}
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores reales a recortar"},{"name":"min","desc":"límite inferior del rango permitido"},{"name":"max","desc":"límite superior del rango permitido"}],"output":"slice con cada valor limitado al rango [min, max]"}


id: detect_outliers_go_datascience
name: detect_outliers
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func DetectOutliers(data []float64, threshold float64) []bool
tags: ["datascience","statistics","outlier","anomaly"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
tests: []
file_path: functions/datascience/detect_outliers.go
created_at: 2026-04-01T01:37:51Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a50ef7c5e654ad78241a89697e22742e3b3b639907d96145283b49a2e8fc225b
description:
  Detecta outliers en un slice de float64 usando z-score. Devuelve true para valores cuyo |z-score| supera el umbral.

code:
  package datascience

  import "math"

  // DetectOutliers devuelve un []bool donde true indica que el valor es un outlier
  // según z-score. Un valor es outlier si |z-score| > threshold.
  func DetectOutliers(data []float64, threshold float64) []bool {
  	n := len(data)
  	if n == 0 {
  		return []bool{}
  	}

  	var sum float64
  	for _, v := range data {
  		sum += v
  	}
  	mean := sum / float64(n)

  	var sqSum float64
  	for _, v := range data {
  		d := v - mean
  		sqSum += d * d
  	}
  	stddev := math.Sqrt(sqSum / float64(n))

  	result := make([]bool, n)
  	if stddev == 0 {
  		return result
  	}
  	for i, v := range data {
  		z := (v - mean) / stddev
  		if z < 0 {
  			z = -z
  		}
  		result[i] = z > threshold
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores numéricos a analizar"},{"name":"threshold","desc":"umbral de z-score para considerar un valor outlier (ej: 2.0 para 2 desviaciones estándar)"}],"output":"slice de booleanos del mismo tamaño que data, true si el elemento es outlier"}


id: diff_entities_go_datascience
name: diff_entities
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func DiffEntities(before, after []map[string]any, key string, ignoreFields []string) map[string]any
tags: ["datascience","diff","entities","operations","snapshot","comparison"]
uses_functions: []
uses_types: []
returns: []
imports: ["fmt"]
test_file_path: functions/datascience/diff_entities_test.go
file_path: functions/datascience/diff_entities.go
created_at: 2026-04-05T14:45:00Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 259451f2c6b28a046866c76e76f8c2d5a0cd7302d0d2e471b84b139eb3dc43fb
description:
  Compara dos snapshots de entities y devuelve diferencias campo a campo. Detecta añadidas, eliminadas, modificadas e inalteradas. Ignora created_at y updated_at por defecto (pasar nil para usar defaults).

example:
  ```go
  before := []map[string]any{
      {"id": "1", "name": "Alice", "status": "active"},
      {"id": "2", "name": "Bob"},
  }
  after := []map[string]any{
      {"id": "1", "name": "Alice", "status": "inactive"},
      {"id": "3", "name": "Carol"},
  }
  result := DiffEntities(before, after, "id", nil)
  // result["summary"] = "1 added, 1 removed, 1 modified, 0 unchanged"
  // result["added"] = [{"id": "3", "name": "Carol"}]
  // result["removed"] = [{"id": "2", "name": "Bob"}]
  // result["modified"] = [{"key": "1", "changes": {"status": {"old": "active", "new": "inactive"}}}]
  ```

tests:
  ["entity añadida","entity eliminada","entity modificada con detalle de campos","entities identicas → unchanged","ignore_fields funciona","lista vacia vs lista con datos","summary format correcto"]

notes:
  Funcion pura. Compara valores con fmt.Sprintf("%v", ...) para manejar tipos heterogeneos en map[string]any.
  ignoreFields nil usa los defaults ["created_at", "updated_at"]. Para no ignorar ningun campo, pasar []string{}.
  Semantica identica a diff_entities_py_datascience, permite comparar resultados entre ejecuciones del mismo pipeline.

code:
  package datascience

  import "fmt"

  // DiffEntities compares two snapshots of entities and returns field-level differences.
  // Detects added, removed, modified, and unchanged entities.
  // ignoreFields specifies fields to exclude from comparison (defaults to ["created_at", "updated_at"] when nil).
  func DiffEntities(before, after []map[string]any, key string, ignoreFields []string) map[string]any {
  	if ignoreFields == nil {
  		ignoreFields = []string{"created_at", "updated_at"}
  	}

  	ignoreSet := make(map[string]bool, len(ignoreFields))
  	for _, f := range ignoreFields {
  		ignoreSet[f] = true
  	}

  	beforeMap := make(map[string]map[string]any, len(before))
  	for _, e := range before {
  		if k, ok := e[key]; ok {
  			beforeMap[fmt.Sprintf("%v", k)] = e
  		}
  	}

  	afterMap := make(map[string]map[string]any, len(after))
  	for _, e := range after {
  		if k, ok := e[key]; ok {
  			afterMap[fmt.Sprintf("%v", k)] = e
  		}
  	}

  	added := []map[string]any{}
  	for k, e := range afterMap {
  		if _, exists := beforeMap[k]; !exists {
  			added = append(added, e)
  		}
  	}

  	removed := []map[string]any{}
  	for k, e := range beforeMap {
  		if _, exists := afterMap[k]; !exists {
  			removed = append(removed, e)
  		}
  	}

  	modified := []map[string]any{}
  	unchanged := 0

  	for k, b := range beforeMap {
  		a, exists := afterMap[k]
  		if !exists {
  			continue
  		}

  		// Collect all fields from both entities
  		allFields := make(map[string]bool)
  		for f := range b {
  			allFields[f] = true
  		}
  		for f := range a {
  			allFields[f] = true
  		}

  		changes := map[string]any{}
  		for field := range allFields {
  			if ignoreSet[field] || field == key {
  				continue
  			}
  			oldVal := b[field]
  			newVal := a[field]
  			if fmt.Sprintf("%v", oldVal) != fmt.Sprintf("%v", newVal) {
  				changes[field] = map[string]any{"old": oldVal, "new": newVal}
  			}
  		}

  		if len(changes) > 0 {
  			modified = append(modified, map[string]any{"key": k, "changes": changes})
  		} else {
  			unchanged++
  		}
  	}

  	nAdded := len(added)
  	nRemoved := len(removed)
  	nModified := len(modified)
  	summary := fmt.Sprintf("%d added, %d removed, %d modified, %d unchanged",
  		nAdded, nRemoved, nModified, unchanged)

  	return map[string]any{
  		"added":     added,
  		"removed":   removed,
  		"modified":  modified,
  		"unchanged": unchanged,
  		"summary":   summary,
  	}
  }

params_schema:
  {"params":[{"name":"before","desc":"slice de snapshots de entities antes (ej: resultado de query anterior)"},{"name":"after","desc":"slice de snapshots de entities después (ej: resultado de query actual)"},{"name":"key","desc":"nombre del campo clave para emparejar entities (ej: 'id', 'uuid')"},{"name":"ignoreFields","desc":"slice de nombres de campos a ignorar en comparación (nil usa defaults ['created_at', 'updated_at'])"}],"output":"mapa con 'summary', 'added', 'removed', 'modified', 'unchanged' - análisis completo de diferencias"}


id: fetch_data_frame_go_datascience
name: fetch_data_frame
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: impure
signature: func FetchDataFrame(dsn, query string) ([]map[string]any, error)
description: Ejecuta una consulta SQL contra un DSN y retorna los resultados como slice de mapas columna-valor.
tags: ["datascience","io","bigquery","fetch"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/datascience/fetch_data_frame.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 85fac7b68eae01b033fb7582204119d119555983adcb172515ee89e56a8c95f9
code:
  package datascience

  import "fmt"

  // FetchDataFrame ejecuta una consulta SQL contra un DSN y retorna los resultados como slice de mapas.
  func FetchDataFrame(dsn, query string) ([]map[string]any, error) {
  	return nil, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"dsn","desc":"connection string para la BD (ej: 'user:pass@tcp(host:3306)/dbname', 'bigquery://project-id')"},{"name":"query","desc":"consulta SQL a ejecutar (ej: 'SELECT * FROM users WHERE status = \"active\"')"}],"output":"slice de mapas columna-valor (ej: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}])"}


id: fft_go_datascience
name: fft
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func FFT(data []float64) []complex128
tags: ["datascience","dsp","fft","fourier","frequency"]
uses_functions: []
uses_types: []
returns: []
imports: ["math","math/cmplx"]
tests: []
file_path: functions/datascience/fft.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 68f899aad944651260f23098260fce1ecdb6b001da378777433151d585a2f3ca
description:
  Calcula la Transformada Rápida de Fourier (FFT) usando el algoritmo Cooley-Tukey radix-2. Aplica zero-padding si la longitud no es potencia de 2.

code:
  package datascience

  import (
  	"math"
  	"math/cmplx"
  )

  // FFT calcula la Fast Fourier Transform usando el algoritmo Cooley-Tukey radix-2.
  // Si la longitud de data no es potencia de 2, se rellena con ceros (zero-padding).
  func FFT(data []float64) []complex128 {
  	n := len(data)
  	if n == 0 {
  		return []complex128{}
  	}

  	// Calcular la siguiente potencia de 2.
  	size := nextPow2(n)

  	// Convertir a complex128 con zero-padding.
  	x := make([]complex128, size)
  	for i := 0; i < n; i++ {
  		x[i] = complex(data[i], 0)
  	}

  	fftRecursive(x)
  	return x
  }

  // nextPow2 retorna la menor potencia de 2 >= n.
  func nextPow2(n int) int {
  	p := 1
  	for p < n {
  		p <<= 1
  	}
  	return p
  }

  // fftRecursive aplica Cooley-Tukey radix-2 DIT in-place.
  func fftRecursive(x []complex128) {
  	n := len(x)
  	if n <= 1 {
  		return
  	}

  	// Separar pares e impares.
  	even := make([]complex128, n/2)
  	odd := make([]complex128, n/2)
  	for i := 0; i < n/2; i++ {
  		even[i] = x[2*i]
  		odd[i] = x[2*i+1]
  	}

  	fftRecursive(even)
  	fftRecursive(odd)

  	for k := 0; k < n/2; k++ {
  		t := cmplx.Rect(1, -2*math.Pi*float64(k)/float64(n)) * odd[k]
  		x[k] = even[k] + t
  		x[k+n/2] = even[k] - t
  	}
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores de serie temporal o señal (ej: [1.0, 2.0, 3.0, ...], se aplica zero-padding si no es potencia de 2)"}],"output":"slice de números complejos representando amplitud y fase en dominio de frecuencia, tamaño potencia de 2"}


id: group_by_go_datascience
name: group_by
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func GroupBy[T any, K comparable](xs []T, keyFn func(T) K) map[K][]T
tags: ["datascience","group","aggregate","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/datascience/group_by.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: fc2c03b906bb4daf544e58157fa44e155438e7dc271f35b28da16fc600fe746f
description:
  Agrupa los elementos de un slice según una función clave, devolviendo un mapa de clave a slice de elementos.

code:
  package datascience

  // GroupBy agrupa los elementos de un slice según la clave devuelta por keyFn.
  func GroupBy[T any, K comparable](xs []T, keyFn func(T) K) map[K][]T {
  	groups := make(map[K][]T)
  	for _, x := range xs {
  		k := keyFn(x)
  		groups[k] = append(groups[k], x)
  	}
  	return groups
  }

params_schema:
  {"params":[{"name":"xs","desc":"slice de elementos a agrupar (tipo genérico T)"},{"name":"keyFn","desc":"función que extrae la clave de agrupación de cada elemento (ej: func(u User) string { return u.Department })"}],"output":"mapa de clave a slice de elementos agrupados (ej: {'Sales': [user1, user2], 'Engineering': [user3]})"}


id: histogram_go_datascience
name: histogram
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func Histogram(data []float64, buckets int) []int
tags: ["datascience","statistics","histogram","frequency"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
tests: []
file_path: functions/datascience/histogram.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d034a75155f824c821375ae920916f288164a5a63a52dda7831f5605e4f8507c
description:
  Calcula las frecuencias de un slice de float64 distribuidas en un número dado de buckets equiespaciados.

code:
  package datascience

  import "math"

  // Histogram calcula las frecuencias de data distribuidas en la cantidad de buckets indicada.
  // Retorna un slice de longitud buckets con el conteo de elementos por cada intervalo equiespaciado.
  func Histogram(data []float64, buckets int) []int {
  	if buckets <= 0 || len(data) == 0 {
  		return make([]int, buckets)
  	}

  	minVal := math.Inf(1)
  	maxVal := math.Inf(-1)
  	for _, v := range data {
  		if v < minVal {
  			minVal = v
  		}
  		if v > maxVal {
  			maxVal = v
  		}
  	}

  	counts := make([]int, buckets)
  	rang := maxVal - minVal
  	if rang == 0 {
  		// Todos los valores son iguales; poner todo en el primer bucket.
  		counts[0] = len(data)
  		return counts
  	}

  	for _, v := range data {
  		idx := int(float64(buckets) * (v - minVal) / rang)
  		if idx >= buckets {
  			idx = buckets - 1
  		}
  		counts[idx]++
  	}
  	return counts
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores numéricos a distribuir en el histograma"},{"name":"buckets","desc":"número de buckets equiespaciados (ej: 10, 50, 100)"}],"output":"slice de enteros con frecuencias en cada bucket, tamaño igual a buckets"}


id: impute_go_datascience
name: impute
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func Impute(data []float64) []float64
tags: ["datascience","impute","missing","fill"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
tests: []
file_path: functions/datascience/impute.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 402d68807b911ea15afd342e44b90cb3ea960fdc2b27349195977353f82b29dc
description:
  Rellena valores NaN en un slice de float64 usando forward-fill, reemplazando cada NaN con el último valor válido anterior.

code:
  package datascience

  import "math"

  // Impute rellena valores NaN usando forward-fill.
  // Cada NaN se reemplaza con el último valor válido (no NaN) anterior.
  // Si el primer valor es NaN y no hay valor anterior, se mantiene como NaN.
  func Impute(data []float64) []float64 {
  	result := make([]float64, len(data))
  	last := math.NaN()
  	for i, v := range data {
  		if !math.IsNaN(v) {
  			last = v
  		}
  		result[i] = last
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores que pueden contener NaN (valores faltantes)"}],"output":"slice sin NaN, cada valor NaN reemplazado por el último valor válido anterior (forward-fill)"}


id: load_csv_go_datascience
name: load_csv
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: impure
signature: func LoadCSV(path string) ([]map[string]string, error)
description: Carga un archivo CSV desde disco y lo retorna como slice de mapas columna-valor.
tags: ["datascience","io","csv","load"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/datascience/load_csv.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 19f1ffebea4e2244c1ff6e10e8eb868c8736af91b4c202f435d041146a0c81b1
code:
  package datascience

  import "fmt"

  // LoadCSV carga un archivo CSV y lo retorna como slice de mapas (columna -> valor).
  func LoadCSV(path string) ([]map[string]string, error) {
  	return nil, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"path","desc":"ruta al archivo CSV (ej: '/data/sales.csv')"}],"output":"slice de mapas donde cada mapa es una fila (columna -\u003e valor), ej: [{'id': '1', 'name': 'Alice', 'sales': '100'}]"}


id: load_parquet_go_datascience
name: load_parquet
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: impure
signature: func LoadParquet(path string) ([]map[string]any, error)
description: Carga un archivo Parquet desde disco y lo retorna como slice de mapas columna-valor.
tags: ["datascience","io","parquet","load"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/datascience/load_parquet.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a21a80cdba11cc6a91ac1084817dc28193f009c1aa81ac6252cc6fa615a3f309
code:
  package datascience

  import "fmt"

  // LoadParquet carga un archivo Parquet y lo retorna como slice de mapas.
  func LoadParquet(path string) ([]map[string]any, error) {
  	return nil, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"path","desc":"ruta al archivo Parquet (ej: '/data/events.parquet')"}],"output":"slice de mapas donde cada mapa es una fila (columna -\u003e valor), ej: [{'user_id': 123, 'timestamp': '2026-01-01T10:00:00'}]"}


id: lorenz_step_go_datascience
name: lorenz_step
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: LorenzStep(s LorenzState, dt float64, p LorenzParams) LorenzState
tags: ["lorenz","chaos","attractor","simulation","math","dynamical-systems"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/datascience/lorenz_step.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b2fc41f04defdcbf8c83a83dafd552ed6b502628960d8ffaeef64b74fa0b7368
description:
  Paso del atractor de Lorenz (sistema caótico determinista). Integración Euler con parámetros configurables. Incluye LorenzSeries para generar N pasos.

example:
  ```go
  p := DefaultLorenzParams()
  state := LorenzState{X: 1, Y: 1, Z: 1}

  // Un paso
  next := LorenzStep(state, 0.005, p)

  // Serie completa
  series := LorenzSeries(LorenzState{X: 1, Y: 1, Z: 1}, 0.005, p, 10000)
  ```

notes:
  El atractor de Lorenz es un sistema de ecuaciones diferenciales que produce comportamiento caótico determinista. Con los parámetros clásicos (sigma=10, rho=28, beta=8/3), el sistema converge al famoso "butterfly attractor". X oscila típicamente entre -20 y 20.

code:
  package datascience

  // LorenzState representa el estado del atractor de Lorenz en 3D.
  type LorenzState struct {
  	X, Y, Z float64
  }

  // LorenzParams define los parámetros del atractor de Lorenz.
  type LorenzParams struct {
  	Sigma float64 // Rate of rotation (default 10)
  	Rho   float64 // Size of attractor (default 28)
  	Beta  float64 // Damping (default 8/3)
  }

  // DefaultLorenzParams retorna los parámetros clásicos del atractor de Lorenz.
  func DefaultLorenzParams() LorenzParams {
  	return LorenzParams{Sigma: 10.0, Rho: 28.0, Beta: 8.0 / 3.0}
  }

  // LorenzStep calcula un paso del atractor de Lorenz usando integración de Euler.
  // dx/dt = sigma*(y-x), dy/dt = x*(rho-z)-y, dz/dt = x*y - beta*z
  func LorenzStep(s LorenzState, dt float64, p LorenzParams) LorenzState {
  	dx := p.Sigma * (s.Y - s.X)
  	dy := s.X*(p.Rho-s.Z) - s.Y
  	dz := s.X*s.Y - p.Beta*s.Z

  	return LorenzState{
  		X: s.X + dx*dt,
  		Y: s.Y + dy*dt,
  		Z: s.Z + dz*dt,
  	}
  }

  // LorenzSeries genera N pasos del atractor y retorna la serie completa.
  func LorenzSeries(initial LorenzState, dt float64, p LorenzParams, steps int) []LorenzState {
  	series := make([]LorenzState, steps)
  	state := initial
  	for i := 0; i < steps; i++ {
  		state = LorenzStep(state, dt, p)
  		series[i] = state
  	}
  	return series
  }

params_schema:
  {"params":[{"name":"s","desc":"estado actual del atractor como {X, Y, Z} (coordenadas 3D del sistema)"},{"name":"dt","desc":"paso temporal de integración (ej: 0.005 segundos, valores menores = más preciso)"},{"name":"p","desc":"parámetros del sistema Lorenz (sigma≈10, rho≈28, beta≈8/3 para el butterfly clásico)"}],"output":"nuevo estado después de un paso de integración Euler, coordenadas típicamente en rango [-20, 20]"}


id: min_max_scale_go_datascience
name: min_max_scale
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func MinMaxScale(data []float64) []float64
description: Escala los valores de un slice al rango [0, 1] usando normalización min-max.
tags: ["datascience","statistics","normalize","scale"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
tests: []
file_path: functions/datascience/min_max_scale.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: bdd9c298810525f56c6972ca5ccaa7496b85d95cb4005941d039cba2db07f448
code:
  package datascience

  import "math"

  // MinMaxScale escala los valores al rango [0, 1] usando min-max normalización.
  // Si min == max, retorna un slice de ceros.
  func MinMaxScale(data []float64) []float64 {
  	n := len(data)
  	if n == 0 {
  		return []float64{}
  	}

  	minVal := math.Inf(1)
  	maxVal := math.Inf(-1)
  	for _, v := range data {
  		if v < minVal {
  			minVal = v
  		}
  		if v > maxVal {
  			maxVal = v
  		}
  	}

  	rang := maxVal - minVal
  	result := make([]float64, n)
  	if rang == 0 {
  		return result
  	}
  	for i, v := range data {
  		result[i] = (v - minVal) / rang
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores numéricos a normalizar"}],"output":"slice escalado al rango [0, 1] donde min(data)→0 y max(data)→1"}


id: pearson_go_datascience
name: pearson
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func Pearson(xs, ys []float64) float64
description: Calcula el coeficiente de correlación de Pearson entre dos slices de float64.
tags: ["datascience","statistics","correlation","pearson"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
tests: []
file_path: functions/datascience/pearson.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 4d896d30d9f4b4aa8794ad86b34fd98efc4536596330fbc5d76908f1ca7fd87f
code:
  package datascience

  import "math"

  // Pearson calcula el coeficiente de correlación de Pearson entre dos slices.
  // Si los slices tienen distinta longitud, usa la longitud mínima.
  // Retorna 0 si alguna desviación estándar es 0.
  func Pearson(xs, ys []float64) float64 {
  	n := len(xs)
  	if len(ys) < n {
  		n = len(ys)
  	}
  	if n == 0 {
  		return 0
  	}

  	var sumX, sumY float64
  	for i := 0; i < n; i++ {
  		sumX += xs[i]
  		sumY += ys[i]
  	}
  	meanX := sumX / float64(n)
  	meanY := sumY / float64(n)

  	var num, denomX, denomY float64
  	for i := 0; i < n; i++ {
  		dx := xs[i] - meanX
  		dy := ys[i] - meanY
  		num += dx * dy
  		denomX += dx * dx
  		denomY += dy * dy
  	}

  	denom := math.Sqrt(denomX * denomY)
  	if denom == 0 {
  		return 0
  	}
  	return num / denom
  }

params_schema:
  {"params":[{"name":"xs","desc":"primer slice de valores (ej: precios del activo X, o variable independiente)"},{"name":"ys","desc":"segundo slice de valores (ej: precios del activo Y, o variable dependiente, mismo tamaño que xs)"}],"output":"coeficiente de correlación de Pearson en rango [-1, 1], donde 1=correlación perfecta, -1=anticorrelación, 0=sin correlación"}


id: pivot_go_datascience
name: pivot
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func Pivot(rows []map[string]any, index, columns, values, agg string) []map[string]any
tags: ["datascience","tabular","pivot","transform","aggregation","go"]
uses_functions: []
uses_types: []
returns: []
imports: []
test_file_path: functions/datascience/pivot_test.go
file_path: functions/datascience/pivot.go
created_at: 2026-04-05T11:25:56Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 83916083c856217e795059e492aaa9829a9c9d667313c2855b590c6cadd180e2
description:
  Pivot table sin dependencias. Agrupa por index, expande valores unicos de columns como nuevas columnas y agrega values con la funcion indicada (sum, count, mean, min, max, first, last). Valores faltantes se rellenan con 0.

example:
  ```go
  rows := []map[string]any{
      {"region": "US", "product": "A", "sales": 10},
      {"region": "US", "product": "B", "sales": 20},
      {"region": "EU", "product": "A", "sales": 15},
  }
  result := Pivot(rows, "region", "product", "sales", "sum")
  // [{"region": "US", "A": 10.0, "B": 20.0}, {"region": "EU", "A": 15.0, "B": 0}]
  ```

tests:
  ["Pivot basico con sum","Pivot con count y mean","Valores faltantes rellenados con 0","Una sola fila","Multiples valores por celda requieren agregacion"]

notes:
  Funcion pura sin dependencias externas. Usa map[string]any para trabajar con datos JSON/SQL deserializados.
  Las agregaciones numericas (sum, mean, min, max) convierten valores a float64 via type assertion.

code:
  package datascience

  // Pivot transforma datos del formato largo al formato ancho (pivot table).
  // Agrupa por index, expande los valores unicos de columns como nuevas columnas
  // y agrega values con la funcion indicada.
  // Funciones de agregacion soportadas: sum, count, mean, min, max, first, last.
  // Valores numericos faltantes se rellenan con 0.
  func Pivot(rows []map[string]any, index, columns, values, agg string) []map[string]any {
  	// Mantener orden de aparicion de index y column values
  	indexOrder := []any{}
  	seenIndex := map[any]bool{}
  	colOrder := []any{}
  	seenCols := map[any]bool{}

  	for _, row := range rows {
  		idx := row[index]
  		col := row[columns]
  		if !seenIndex[idx] {
  			seenIndex[idx] = true
  			indexOrder = append(indexOrder, idx)
  		}
  		if !seenCols[col] {
  			seenCols[col] = true
  			colOrder = append(colOrder, col)
  		}
  	}

  	// Acumular: groups[indexVal][colVal] = lista de valores
  	type key struct{ idx, col any }
  	groups := map[key][]any{}
  	for _, row := range rows {
  		idx := row[index]
  		col := row[columns]
  		val := row[values]
  		if val != nil {
  			k := key{idx, col}
  			groups[k] = append(groups[k], val)
  		}
  	}

  	aggregate := func(vals []any, fn string) any {
  		if len(vals) == 0 {
  			return 0
  		}
  		switch fn {
  		case "count":
  			return len(vals)
  		case "first":
  			return vals[0]
  		case "last":
  			return vals[len(vals)-1]
  		}
  		// Funciones numericas: sum, mean, min, max
  		toFloat := func(v any) float64 {
  			switch n := v.(type) {
  			case float64:
  				return n
  			case float32:
  				return float64(n)
  			case int:
  				return float64(n)
  			case int64:
  				return float64(n)
  			case int32:
  				return float64(n)
  			}
  			return 0
  		}
  		sum := 0.0
  		mn := toFloat(vals[0])
  		mx := toFloat(vals[0])
  		for _, v := range vals {
  			f := toFloat(v)
  			sum += f
  			if f < mn {
  				mn = f
  			}
  			if f > mx {
  				mx = f
  			}
  		}
  		switch fn {
  		case "sum":
  			return sum
  		case "mean":
  			return sum / float64(len(vals))
  		case "min":
  			return mn
  		case "max":
  			return mx
  		}
  		return sum
  	}

  	result := make([]map[string]any, 0, len(indexOrder))
  	for _, idx := range indexOrder {
  		record := map[string]any{index: idx}
  		for _, col := range colOrder {
  			k := key{idx, col}
  			vals := groups[k]
  			if len(vals) > 0 {
  				record[col.(string)] = aggregate(vals, agg)
  			} else {
  				record[col.(string)] = 0
  			}
  		}
  		result = append(result, record)
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"rows","desc":"slice de mapas columna-valor (ej: resultado de load_csv o fetch_data_frame)"},{"name":"index","desc":"nombre de columna que se convierte en fila (ej: 'region')"},{"name":"columns","desc":"nombre de columna cuyos valores únicos se convierten en nuevas columnas (ej: 'product')"},{"name":"values","desc":"nombre de columna con valores a agregar (ej: 'sales')"},{"name":"agg","desc":"función de agregación: 'sum', 'count', 'mean', 'min', 'max', 'first', 'last'"}],"output":"slice de mapas pivotados donde cada fila agrupa por index, con nuevas columnas por cada valor unique de columns"}


id: rolling_window_go_datascience
name: rolling_window
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func RollingWindow[T any](xs []T, size int) [][]T
description: Genera ventanas deslizantes de tamaño fijo sobre un slice genérico.
tags: ["datascience","window","rolling","sliding","generic"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/datascience/rolling_window.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c7355dfc006affba5bbfa9e63b8e8e8ff6c8d63c55e3fc1800a3edb3585e54b6
code:
  package datascience

  // RollingWindow genera ventanas deslizantes de tamaño size sobre el slice xs.
  // Si size <= 0 o size > len(xs), retorna nil.
  func RollingWindow[T any](xs []T, size int) [][]T {
  	n := len(xs)
  	if size <= 0 || size > n {
  		return nil
  	}
  	windows := make([][]T, 0, n-size+1)
  	for i := 0; i <= n-size; i++ {
  		w := make([]T, size)
  		copy(w, xs[i:i+size])
  		windows = append(windows, w)
  	}
  	return windows
  }

params_schema:
  {"params":[{"name":"xs","desc":"slice de elementos a ventanar (tipo genérico T)"},{"name":"size","desc":"tamaño de cada ventana deslizante (ej: 10 para ventanas de 10 elementos)"}],"output":"slice de slices donde cada inner slice es una ventana de tamaño size, total ventanas = len(xs) - size + 1"}


id: standardize_go_datascience
name: standardize
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func Standardize(data []float64) []float64
tags: ["datascience","statistics","normalize","zscore"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
tests: []
file_path: functions/datascience/standardize.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f0e86801708f45df0a527c1abea27337c5c8f79d306e6d6d62fc012aac064648
description:
  Aplica Z-score normalización a un slice de float64, transformando cada valor a (x - media) / desviación estándar.

code:
  package datascience

  import "math"

  // Standardize aplica Z-score normalización a un slice de float64.
  // Cada valor se transforma a (x - mean) / stddev.
  // Si stddev es 0, retorna un slice de ceros.
  func Standardize(data []float64) []float64 {
  	n := len(data)
  	if n == 0 {
  		return []float64{}
  	}

  	var sum float64
  	for _, v := range data {
  		sum += v
  	}
  	mean := sum / float64(n)

  	var sqSum float64
  	for _, v := range data {
  		d := v - mean
  		sqSum += d * d
  	}
  	stddev := math.Sqrt(sqSum / float64(n))

  	result := make([]float64, n)
  	if stddev == 0 {
  		return result
  	}
  	for i, v := range data {
  		result[i] = (v - mean) / stddev
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de valores numéricos a normalizar"}],"output":"slice estandarizado donde cada valor x → (x - media) / desviación_estándar, media=0 y desviación_estándar=1"}


id: zip_slices_go_datascience
name: zip_slices
kind: function
lang: go
domain: datascience
version: 1.0.0
purity: pure
signature: func ZipSlices(as, bs []float64) [][2]float64
description: Combina dos slices de float64 en un slice de pares [2]float64, truncando al más corto.
tags: ["datascience","zip","combine","pair"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/datascience/zip_slices.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: dc8ab54aa02c0c1c9ec845b0a3773186888f557cbffed94e1912de7b28ba08f3
code:
  package datascience

  // ZipSlices combina dos slices de float64 en pares [2]float64.
  // El resultado tiene longitud igual al menor de los dos slices.
  func ZipSlices(as, bs []float64) [][2]float64 {
  	n := len(as)
  	if len(bs) < n {
  		n = len(bs)
  	}
  	result := make([][2]float64, n)
  	for i := 0; i < n; i++ {
  		result[i] = [2]float64{as[i], bs[i]}
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"as","desc":"primer slice de valores (ej: precios)"},{"name":"bs","desc":"segundo slice de valores (ej: volúmenes)"}],"output":"slice de pares [2]float64 donde cada par es [as[i], bs[i]], longitud = min(len(as), len(bs))"}


id: annualized_volatility_go_finance
name: annualized_volatility
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func AnnualizedVolatility(returns []float64, periodsPerYear float64) float64
tags: ["finance","volatility","risk","annualized"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
example:
  ```go
  vol := finance.AnnualizedVolatility([]float64{0.01, -0.02, 0.015, 0.005, -0.01}, 252)
  ```

tests: []
file_path: functions/finance/annualized_volatility.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 8f0dbf0d361e9098497b25e8102faf458a29ac5697a4d2cce938c43dffd32ef2
description:
  Calcula la volatilidad anualizada a partir de una serie de retornos y la frecuencia de los periodos.

documentation:
  # annualized_volatility

  Calcula la volatilidad anualizada como `stddev(returns) * sqrt(periodsPerYear)`. Usa desviacion estandar muestral (n-1).

code:
  package finance

  import "math"

  // AnnualizedVolatility calcula la volatilidad anualizada a partir de una serie de retornos.
  // periodsPerYear indica cuantos periodos hay en un anio (e.g. 252 para retornos diarios).
  // Retorna stddev(returns) * sqrt(periodsPerYear).
  func AnnualizedVolatility(returns []float64, periodsPerYear float64) float64 {
  	n := len(returns)
  	if n < 2 {
  		return 0
  	}
  	var sum float64
  	for _, r := range returns {
  		sum += r
  	}
  	mean := sum / float64(n)
  	var variance float64
  	for _, r := range returns {
  		diff := r - mean
  		variance += diff * diff
  	}
  	variance /= float64(n - 1)
  	return math.Sqrt(variance) * math.Sqrt(periodsPerYear)
  }

params_schema:
  {"params":[{"name":"returns","desc":"slice de retornos diarios/periódicos, valores típicamente entre -0.1 y 0.1 (ej: -2% = -0.02)"},{"name":"periodsPerYear","desc":"número de períodos por año (252 para diarios, 12 para mensuales, 52 para semanales)"}],"output":"volatilidad anualizada como ratio (ej: 0.25 = 25% volatilidad anual)"}


id: bollinger_bands_go_finance
name: bollinger_bands
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func BollingerBands(data []float64, period int, numStdDev float64) (upper, middle, lower []float64)
description: Calcula las bandas de Bollinger (upper, middle, lower) para una serie de precios.
tags: ["finance","indicator","bollinger","bands"]
uses_functions: []
uses_types: ["bollinger_result_go_finance"]
returns: ["bollinger_result_go_finance"]
imports: ["math"]
tests: []
file_path: functions/finance/bollinger_bands.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1a8dd66a54a88591a532c97b8d0cd9e771a5ec37c42b30390b1faf7cfd02af7d
example:
  ```go
  upper, middle, lower := finance.BollingerBands(
      []float64{22, 24, 23, 25, 26, 28, 27, 29, 30, 28},
      5, 2.0,
  )
  ```

documentation:
  # bollinger_bands

  Calcula las bandas de Bollinger. La banda media es la SMA, y las bandas superior e inferior estan a `numStdDev` desviaciones estandar de la media. Usa desviacion estandar poblacional.

code:
  package finance

  import "math"

  // BollingerBands calcula las bandas de Bollinger: upper, middle (SMA), lower.
  // Los primeros period-1 elementos de cada slice son 0.
  func BollingerBands(data []float64, period int, numStdDev float64) (upper, middle, lower []float64) {
  	n := len(data)
  	upper = make([]float64, n)
  	middle = make([]float64, n)
  	lower = make([]float64, n)
  	if period <= 0 || period > n {
  		return
  	}
  	for i := period - 1; i < n; i++ {
  		var sum float64
  		for j := i - period + 1; j <= i; j++ {
  			sum += data[j]
  		}
  		mean := sum / float64(period)
  		var variance float64
  		for j := i - period + 1; j <= i; j++ {
  			diff := data[j] - mean
  			variance += diff * diff
  		}
  		std := math.Sqrt(variance / float64(period))
  		middle[i] = mean
  		upper[i] = mean + numStdDev*std
  		lower[i] = mean - numStdDev*std
  	}
  	return
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de precios de cierre (ej: [22.5, 23.1, 22.8, ...])"},{"name":"period","desc":"período de la media móvil simple (ej: 5, 20, 50 días)"},{"name":"numStdDev","desc":"número de desviaciones estándar para las bandas (típicamente 2.0)"}],"output":"tupla (upper, middle, lower) - tres slices de precios banda superior, media (SMA) e inferior"}


id: ema_go_finance
name: ema
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func EMA(data []float64, period int) []float64
description: Calcula la media movil exponencial (EMA) sobre una serie de datos con un periodo dado.
tags: ["finance","indicator","ema","moving-average"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/finance/ema.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: fa63fbd04b697326e7d87f52f936754845e9cf0592c9bc27e18a50ff959e4f5c
example:
  ```go
  result := finance.EMA([]float64{10, 11, 12, 13, 14, 15}, 3)
  // result[2] = SMA de los primeros 3 = 11.0
  // result[3] en adelante usa suavizado exponencial
  ```

documentation:
  # ema

  Calcula la media movil exponencial (Exponential Moving Average). Se inicializa con la SMA de los primeros `period` elementos. El multiplicador es `2 / (period + 1)`.

code:
  package finance

  // EMA calcula la media movil exponencial de data con el periodo dado.
  // El primer valor valido se inicializa con la SMA de los primeros period elementos.
  // Los primeros period-1 elementos del resultado son 0.
  func EMA(data []float64, period int) []float64 {
  	n := len(data)
  	result := make([]float64, n)
  	if period <= 0 || period > n {
  		return result
  	}
  	k := 2.0 / float64(period+1)
  	// Seed con SMA
  	var sum float64
  	for i := 0; i < period; i++ {
  		sum += data[i]
  	}
  	result[period-1] = sum / float64(period)
  	for i := period; i < n; i++ {
  		result[i] = data[i]*k + result[i-1]*(1-k)
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de precios de cierre o valores de serie temporal (ej: [10.0, 11.0, 12.0, ...])"},{"name":"period","desc":"período de suavizado (ej: 5, 12, 26 días), determina multiplicador 2/(period+1)"}],"output":"slice de valores EMA del mismo tamaño que data, primeros period-1 elementos son 0"}


id: fetch_ohlcv_go_finance
name: fetch_ohlcv
kind: function
lang: go
domain: finance
version: 1.0.0
purity: impure
signature: func FetchOHLCV(symbol, interval string) ([][]float64, error)
description: Obtiene datos OHLCV de un exchange para un simbolo e intervalo dados.
tags: ["finance","io","exchange","fetch"]
uses_functions: []
uses_types: ["ohlcv_go_finance"]
returns: ["ohlcv_go_finance"]
error_type: error_go_core
imports: ["fmt"]
example:
  ```go
  data, err := finance.FetchOHLCV("BTC/USDT", "1h")
  ```

tests: []
file_path: functions/finance/fetch_ohlcv.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
documentation:
  # fetch_ohlcv

  Stub para obtener datos OHLCV de un exchange. Pendiente de implementacion.

content_hash: 3e08c4c7c8c427e936d12dc4a42acf2010abbc313ffe58d34592a766b3cb9425
code:
  package finance

  import "fmt"

  // FetchOHLCV obtiene datos OHLCV de un exchange para un simbolo e intervalo dados.
  // TODO: implementar conexion real al exchange.
  func FetchOHLCV(symbol, interval string) ([][]float64, error) {
  	return nil, fmt.Errorf("not implemented: FetchOHLCV(%s, %s)", symbol, interval)
  }

params_schema:
  {"params":[{"name":"symbol","desc":"símbolo del instrumento (ej: 'BTC/USDT', 'EUR/USD', 'AAPL')"},{"name":"interval","desc":"intervalo temporal (ej: '1m', '5m', '1h', '1d')"}],"output":"slice de slices de 5 floats en orden [open, high, low, close, volume] por cada vela"}


id: load_ohlcv_from_duckdb_go_finance
name: load_ohlcv_from_duckdb
kind: function
lang: go
domain: finance
version: 1.0.0
purity: impure
signature: func LoadOHLCVFromDuckDB(dbPath, query string) ([][]float64, error)
description: Carga datos OHLCV ejecutando una query SQL en una base de datos DuckDB.
tags: ["finance","io","duckdb","load"]
uses_functions: []
uses_types: ["ohlcv_go_finance"]
returns: ["ohlcv_go_finance"]
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/finance/load_ohlcv_from_duckdb.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
documentation:
  # load_ohlcv_from_duckdb

  Stub para cargar datos OHLCV desde DuckDB. Pendiente de implementacion.

content_hash: 68941bcddc6f05d47caf1a2efbc2aa11b0e99ec411c2ed10b75c4f392c735de8
example:
  ```go
  data, err := finance.LoadOHLCVFromDuckDB("/data/market.duckdb", "SELECT * FROM ohlcv WHERE symbol='BTC'")
  ```

code:
  package finance

  import "fmt"

  // LoadOHLCVFromDuckDB carga datos OHLCV ejecutando una query en una base DuckDB.
  // TODO: implementar conexion real a DuckDB.
  func LoadOHLCVFromDuckDB(dbPath, query string) ([][]float64, error) {
  	return nil, fmt.Errorf("not implemented: LoadOHLCVFromDuckDB(%s, %s)", dbPath, query)
  }

params_schema:
  {"params":[{"name":"dbPath","desc":"ruta al archivo DuckDB (ej: '/data/market.duckdb')"},{"name":"query","desc":"consulta SQL para extraer filas (ej: 'SELECT open, high, low, close, volume FROM ohlcv WHERE symbol=\"BTC\"')"}],"output":"slice de slices de 5 floats en orden [open, high, low, close, volume] por cada fila de resultado"}


id: log_return_go_finance
name: log_return
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func LogReturn(priceStart, priceEnd float64) float64
description: Calcula el retorno logaritmico entre un precio inicial y un precio final.
tags: ["finance","return","logarithmic","atomic"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
example:
  ```go
  r := finance.LogReturn(100.0, 110.0)
  // r ~ 0.09531 (aprox 9.53%)
  ```

tests: []
file_path: functions/finance/log_return.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 977a781dde9aae4f15ad546c0070980b5b7c51f5e3b5254a779d3c8a89b09026
documentation:
  # log_return

  Calcula el retorno logaritmico: `ln(priceEnd / priceStart)`. Si alguno de los precios es <= 0, retorna 0.

code:
  package finance

  import "math"

  // LogReturn calcula el retorno logaritmico entre dos precios.
  // Retorna ln(priceEnd / priceStart). Si priceStart <= 0 o priceEnd <= 0, retorna 0.
  func LogReturn(priceStart, priceEnd float64) float64 {
  	if priceStart <= 0 || priceEnd <= 0 {
  		return 0
  	}
  	return math.Log(priceEnd / priceStart)
  }

params_schema:
  {"params":[{"name":"priceStart","desc":"precio inicial en unidad de moneda (ej: 100.0 USD)"},{"name":"priceEnd","desc":"precio final en unidad de moneda (ej: 110.0 USD)"}],"output":"retorno logarítmico sin dimensión (ej: 0.09531 ≈ 9.53%), devuelve 0 si precios \u003c= 0"}


id: max_drawdown_go_finance
name: max_drawdown
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func MaxDrawdown(values []float64) (maxDD float64, start, end int)
description: Calcula el maximo drawdown de una curva de equity, retornando la magnitud y los indices pico-valle.
tags: ["finance","drawdown","risk","metric"]
uses_functions: []
uses_types: ["drawdown_result_go_finance"]
returns: ["drawdown_result_go_finance"]
imports: []
tests: []
file_path: functions/finance/max_drawdown.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 6779daff04780ca7332d2a656e8e8152ac12ecf0aef82d1cca516aad6fb99c33
example:
  ```go
  dd, s, e := finance.MaxDrawdown([]float64{100, 120, 90, 110, 80})
  // dd = (120-80)/120 = 0.3333, s = 1, e = 4
  ```

documentation:
  # max_drawdown

  Calcula el maximo drawdown (caida maxima desde un pico hasta un valle) como fraccion del pico. Retorna la magnitud (0 a 1) y los indices de inicio y fin.

code:
  package finance

  // MaxDrawdown calcula el maximo drawdown de una serie de valores (e.g. equity curve).
  // Retorna la magnitud del drawdown (valor positivo entre 0 y 1 como fraccion del pico),
  // y los indices de inicio (pico) y fin (valle) del peor drawdown.
  // Si la serie tiene menos de 2 elementos, retorna 0, 0, 0.
  func MaxDrawdown(values []float64) (maxDD float64, start, end int) {
  	n := len(values)
  	if n < 2 {
  		return 0, 0, 0
  	}
  	peakIdx := 0
  	peak := values[0]
  	for i := 1; i < n; i++ {
  		if values[i] > peak {
  			peak = values[i]
  			peakIdx = i
  		}
  		dd := (peak - values[i]) / peak
  		if dd > maxDD {
  			maxDD = dd
  			start = peakIdx
  			end = i
  		}
  	}
  	return
  }

params_schema:
  {"params":[{"name":"values","desc":"slice de valores de equity curve o net worth (ej: [1000, 1200, 900, 1100, 800])"}],"output":"tupla (maxDD, start, end) - drawdown como ratio [0,1] (ej: 0.333 = 33.3%), índice de pico, índice de valle"}


id: normalize_ohlcv_go_finance
name: normalize_ohlcv
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
description: Ajusta slices de precios OHLCV multiplicando cada valor por un factor dado.
tags: ["finance","ohlcv","normalize","adjust"]
uses_functions: []
uses_types: ["ohlcv_go_finance"]
returns: ["ohlcv_go_finance"]
imports: []
tests: []
file_path: functions/finance/normalize_ohlcv.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: aa53b77a4bb4ca8b43c5022893d4e4a33e5d08e2de7a4d8a752149b28c835f9b
signature:
  func NormalizeOHLCV(open, high, low, close []float64, factor float64) ([]float64, []float64, []float64, []float64)

example:
  ```go
  o, h, l, c := finance.NormalizeOHLCV(
      []float64{100, 200},
      []float64{110, 210},
      []float64{90, 190},
      []float64{105, 205},
      2.0,
  )
  // o = [200, 400], h = [220, 420], l = [180, 380], c = [210, 410]
  ```

documentation:
  # normalize_ohlcv

  Ajusta slices de precios OHLCV (open, high, low, close) multiplicando cada elemento por un factor escalar. Util para ajustes por splits, conversiones de divisa, o normalizacion de series.

code:
  package finance

  // NormalizeOHLCV ajusta slices de precios OHLCV multiplicando por un factor.
  func NormalizeOHLCV(open, high, low, close []float64, factor float64) ([]float64, []float64, []float64, []float64) {
  	n := len(open)
  	nOpen := make([]float64, n)
  	nHigh := make([]float64, n)
  	nLow := make([]float64, n)
  	nClose := make([]float64, n)
  	for i := 0; i < n; i++ {
  		nOpen[i] = open[i] * factor
  		nHigh[i] = high[i] * factor
  		nLow[i] = low[i] * factor
  		nClose[i] = close[i] * factor
  	}
  	return nOpen, nHigh, nLow, nClose
  }

params_schema:
  {"params":[{"name":"open","desc":"slice de precios de apertura"},{"name":"high","desc":"slice de precios máximos"},{"name":"low","desc":"slice de precios mínimos"},{"name":"close","desc":"slice de precios de cierre"},{"name":"factor","desc":"multiplicador escalar (ej: 2.0 para duplicar precios, 1/100 para convertir centavos a dólares)"}],"output":"tupla (open_adj, high_adj, low_adj, close_adj) - cuatro slices de precios ajustados"}


id: rsi_go_finance
name: rsi
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func RSI(data []float64, period int) []float64
description: Calcula el Relative Strength Index (RSI) usando suavizado de Wilder.
tags: ["finance","indicator","rsi","momentum"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/finance/rsi.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 0fedd7b05679a29db11769b7f0fed2d621ec4f8f1bff1846c717215560fda16b
example:
  ```go
  result := finance.RSI([]float64{44, 44.34, 44.09, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84}, 5)
  ```

documentation:
  # rsi

  Calcula el Relative Strength Index (RSI) con el metodo de suavizado de Wilder. Los primeros `period` elementos son 0. El RSI oscila entre 0 y 100.

code:
  package finance

  // RSI calcula el Relative Strength Index.
  // Usa el metodo de suavizado de Wilder (EMA con alpha = 1/period).
  // Los primeros period elementos del resultado son 0.
  func RSI(data []float64, period int) []float64 {
  	n := len(data)
  	result := make([]float64, n)
  	if period <= 0 || n < period+1 {
  		return result
  	}
  	var gainSum, lossSum float64
  	for i := 1; i <= period; i++ {
  		change := data[i] - data[i-1]
  		if change > 0 {
  			gainSum += change
  		} else {
  			lossSum -= change
  		}
  	}
  	avgGain := gainSum / float64(period)
  	avgLoss := lossSum / float64(period)
  	if avgLoss == 0 {
  		result[period] = 100
  	} else {
  		rs := avgGain / avgLoss
  		result[period] = 100 - 100/(1+rs)
  	}
  	for i := period + 1; i < n; i++ {
  		change := data[i] - data[i-1]
  		var gain, loss float64
  		if change > 0 {
  			gain = change
  		} else {
  			loss = -change
  		}
  		avgGain = (avgGain*float64(period-1) + gain) / float64(period)
  		avgLoss = (avgLoss*float64(period-1) + loss) / float64(period)
  		if avgLoss == 0 {
  			result[i] = 100
  		} else {
  			rs := avgGain / avgLoss
  			result[i] = 100 - 100/(1+rs)
  		}
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de precios de cierre o valores de serie temporal"},{"name":"period","desc":"período de cálculo (ej: 5, 14, 21 días)"}],"output":"slice de valores RSI del mismo tamaño que data, rango [0, 100], primeros period elementos son 0"}


id: sharpe_ratio_go_finance
name: sharpe_ratio
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func SharpeRatio(returns []float64, riskFreeRate float64, periodsPerYear float64) float64
description: Calcula el ratio de Sharpe anualizado a partir de retornos, tasa libre de riesgo y frecuencia.
tags: ["finance","sharpe","risk","ratio"]
uses_functions: []
uses_types: []
returns: []
imports: ["math"]
example:
  ```go
  sr := finance.SharpeRatio([]float64{0.01, 0.02, -0.005, 0.015, 0.008}, 0.0001, 252)
  ```

tests: []
file_path: functions/finance/sharpe_ratio.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 765970b0e9dc6ad1a804a0b4f064c05fc252d11133c776aec5f3c10de4bf5693
documentation:
  # sharpe_ratio

  Calcula el ratio de Sharpe: `(mean(returns) - riskFreeRate) / stddev(returns) * sqrt(periodsPerYear)`. Usa desviacion estandar muestral.

code:
  package finance

  import "math"

  // SharpeRatio calcula el ratio de Sharpe.
  // riskFreeRate es la tasa libre de riesgo por periodo.
  // periodsPerYear indica cuantos periodos hay en un anio (e.g. 252 para diario).
  // Formula: (mean(returns) - riskFreeRate) / stddev(returns) * sqrt(periodsPerYear)
  func SharpeRatio(returns []float64, riskFreeRate float64, periodsPerYear float64) float64 {
  	n := len(returns)
  	if n < 2 {
  		return 0
  	}
  	var sum float64
  	for _, r := range returns {
  		sum += r
  	}
  	mean := sum / float64(n)
  	var variance float64
  	for _, r := range returns {
  		diff := r - mean
  		variance += diff * diff
  	}
  	std := math.Sqrt(variance / float64(n-1))
  	if std == 0 {
  		return 0
  	}
  	return (mean - riskFreeRate) / std * math.Sqrt(periodsPerYear)
  }

params_schema:
  {"params":[{"name":"returns","desc":"slice de retornos periódicos (ej: [0.01, -0.02, 0.015, 0.008])"},{"name":"riskFreeRate","desc":"tasa libre de riesgo por período (ej: 0.0001 para 0.01% diario)"},{"name":"periodsPerYear","desc":"número de períodos por año (252 para diarios, 12 para mensuales)"}],"output":"ratio de Sharpe anualizado sin dimensión (ej: 1.5 = 1.5 unidades de retorno por unidad de riesgo)"}


id: sma_go_finance
name: sma
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func SMA(data []float64, period int) []float64
description: Calcula la media movil simple (SMA) sobre una serie de datos con un periodo dado.
tags: ["finance","indicator","sma","moving-average"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  result := finance.SMA([]float64{1, 2, 3, 4, 5}, 3)
  // result = [0, 0, 2, 3, 4]
  ```

tests: []
file_path: functions/finance/sma.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: dfb44ceda4480cc20e9ddc3ef8d1ea58fb0b30bf8a1d0155b0612b2e5c3fb3ad
documentation:
  # sma

  Calcula la media movil simple (Simple Moving Average). Los primeros `period-1` elementos del resultado son 0 ya que no hay datos suficientes para calcular la media.

code:
  package finance

  // SMA calcula la media movil simple de data con el periodo dado.
  // Retorna un slice de longitud len(data) donde los primeros period-1
  // elementos son 0 (sin datos suficientes).
  func SMA(data []float64, period int) []float64 {
  	n := len(data)
  	result := make([]float64, n)
  	if period <= 0 || period > n {
  		return result
  	}
  	var sum float64
  	for i := 0; i < period; i++ {
  		sum += data[i]
  	}
  	result[period-1] = sum / float64(period)
  	for i := period; i < n; i++ {
  		sum += data[i] - data[i-period]
  		result[i] = sum / float64(period)
  	}
  	return result
  }

params_schema:
  {"params":[{"name":"data","desc":"slice de precios de cierre o valores de serie temporal (ej: [1.0, 2.0, 3.0, ...])"},{"name":"period","desc":"período de media móvil (ej: 5, 20, 50, 200 días)"}],"output":"slice de valores SMA del mismo tamaño que data, primeros period-1 elementos son 0 (sin datos suficientes)"}


id: stream_ticks_go_finance
name: stream_ticks
kind: function
lang: go
domain: finance
version: 1.0.0
purity: impure
signature: func StreamTicks(symbol string) (<-chan [2]float64, error)
description: Abre un stream de ticks en tiempo real para un simbolo via websocket.
tags: ["finance","io","stream","realtime"]
uses_functions: []
uses_types: ["tick_go_finance"]
returns: ["tick_go_finance"]
error_type: error_go_core
imports: ["fmt"]
example:
  ```go
  ch, err := finance.StreamTicks("ETH/USDT")
  ```

tests: []
file_path: functions/finance/stream_ticks.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
documentation:
  # stream_ticks

  Stub para abrir un stream de ticks en tiempo real. Pendiente de implementacion.

content_hash: 2fd427c70c5ddbb4448f06c7ec48d2313b3686173152175b4741f0b60e478971
code:
  package finance

  import "fmt"

  // StreamTicks abre un stream de ticks en tiempo real para un simbolo.
  // Retorna un canal de [2]float64 donde [0] es precio y [1] es volumen.
  // TODO: implementar conexion real via websocket.
  func StreamTicks(symbol string) (<-chan [2]float64, error) {
  	return nil, fmt.Errorf("not implemented: StreamTicks(%s)", symbol)
  }

params_schema:
  {"params":[{"name":"symbol","desc":"símbolo del instrumento (ej: 'BTC/USDT', 'EUR/USD')"}],"output":"canal que emite pares [2]float64 de [precio, volumen] en cada tick en tiempo real"}


id: tick_to_ohlcv_go_finance
name: tick_to_ohlcv
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
description: Agrega datos de ticks en velas OHLCV segun un intervalo de tiempo en segundos.
tags: ["finance","ohlcv","aggregate","tick"]
uses_functions: []
uses_types: ["tick_go_finance","ohlcv_go_finance"]
returns: ["ohlcv_go_finance"]
imports: []
tests: []
file_path: functions/finance/tick_to_ohlcv.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 8043209bccf6a99d5a27b213e259654044a6fb68d8c8c752752ca2094a8ed2e5
signature:
  func TickToOHLCV(prices, volumes []float64, timestamps []int64, intervalSecs int64) (open, high, low, close, vol []float64)

example:
  ```go
  o, h, l, c, v := finance.TickToOHLCV(
      []float64{100, 101, 99, 102},
      []float64{10, 20, 15, 25},
      []int64{1000, 1005, 1055, 1060},
      60,
  )
  // Genera 2 velas: [1000-1059] y [1060-1119]
  ```

documentation:
  # tick_to_ohlcv

  Agrega ticks (precio, volumen, timestamp) en velas OHLCV. Los ticks se agrupan en buckets de `intervalSecs` segundos. Los timestamps deben estar en unix seconds y ordenados cronologicamente.

code:
  package finance

  // TickToOHLCV agrega datos de ticks en velas OHLCV segun un intervalo en segundos.
  // prices, volumes y timestamps deben tener la misma longitud.
  // timestamps son unix seconds. intervalSecs define el ancho de cada vela.
  // Retorna slices de open, high, low, close, volume para cada vela generada.
  func TickToOHLCV(prices, volumes []float64, timestamps []int64, intervalSecs int64) (open, high, low, close, vol []float64) {
  	n := len(prices)
  	if n == 0 || intervalSecs <= 0 {
  		return
  	}
  	bucketStart := timestamps[0] - (timestamps[0] % intervalSecs)
  	o := prices[0]
  	h := prices[0]
  	l := prices[0]
  	c := prices[0]
  	v := volumes[0]

  	for i := 1; i < n; i++ {
  		currentBucket := timestamps[i] - (timestamps[i] % intervalSecs)
  		if currentBucket != bucketStart {
  			// Cerrar la vela anterior
  			open = append(open, o)
  			high = append(high, h)
  			low = append(low, l)
  			close = append(close, c)
  			vol = append(vol, v)
  			// Nueva vela
  			bucketStart = currentBucket
  			o = prices[i]
  			h = prices[i]
  			l = prices[i]
  			c = prices[i]
  			v = volumes[i]
  		} else {
  			if prices[i] > h {
  				h = prices[i]
  			}
  			if prices[i] < l {
  				l = prices[i]
  			}
  			c = prices[i]
  			v += volumes[i]
  		}
  	}
  	// Cerrar ultima vela
  	open = append(open, o)
  	high = append(high, h)
  	low = append(low, l)
  	close = append(close, c)
  	vol = append(vol, v)
  	return
  }

params_schema:
  {"params":[{"name":"prices","desc":"slice de precios de ticks en orden cronológico"},{"name":"volumes","desc":"slice de volúmenes de ticks (mismo tamaño que prices)"},{"name":"timestamps","desc":"slice de timestamps en unix seconds (mismo tamaño que prices, debe estar ordenado)"},{"name":"intervalSecs","desc":"intervalo de agregación en segundos (ej: 60 para 1 minuto, 3600 para 1 hora)"}],"output":"tupla (open, high, low, close, volume) - cinco slices de datos OHLCV agregados por intervalo"}


id: vwap_go_finance
name: vwap
kind: function
lang: go
domain: finance
version: 1.0.0
purity: pure
signature: func VWAP(prices, volumes []float64) float64
description: Calcula el Volume Weighted Average Price (VWAP) a partir de precios y volumenes.
tags: ["finance","indicator","vwap","volume"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/finance/vwap.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 220b5cc89026ce19142cd9fdbc48fe4f30e279d05e84f04517f4e20801f7e5c6
example:
  ```go
  v := finance.VWAP([]float64{100, 102, 101}, []float64{500, 300, 200})
  // v = (100*500 + 102*300 + 101*200) / (500+300+200)
  ```

documentation:
  # vwap

  Calcula el VWAP (Volume Weighted Average Price). Es la suma de (precio * volumen) dividida por la suma de volumenes.

code:
  package finance

  // VWAP calcula el Volume Weighted Average Price.
  // prices y volumes deben tener la misma longitud.
  // Retorna 0 si el volumen total es 0 o los slices estan vacios.
  func VWAP(prices, volumes []float64) float64 {
  	if len(prices) == 0 || len(prices) != len(volumes) {
  		return 0
  	}
  	var cumPV, cumV float64
  	for i := range prices {
  		cumPV += prices[i] * volumes[i]
  		cumV += volumes[i]
  	}
  	if cumV == 0 {
  		return 0
  	}
  	return cumPV / cumV
  }

params_schema:
  {"params":[{"name":"prices","desc":"slice de precios de ticks o velas (ej: [100.0, 102.0, 101.0])"},{"name":"volumes","desc":"slice de volúmenes correspondientes (mismo tamaño que prices)"}],"output":"precio promedio ponderado por volumen en unidad de moneda (ej: 100.95)"}


id: write_ohlcv_to_parquet_go_finance
name: write_ohlcv_to_parquet
kind: function
lang: go
domain: finance
version: 1.0.0
purity: impure
signature: func WriteOHLCVToParquet(path string, data [][]float64) error
description: Persiste datos OHLCV en un archivo Parquet en la ruta indicada.
tags: ["finance","io","parquet","persist"]
uses_functions: []
uses_types: ["ohlcv_go_finance"]
returns: []
error_type: error_go_core
imports: ["fmt"]
example:
  ```go
  err := finance.WriteOHLCVToParquet("/data/btc_1h.parquet", ohlcvData)
  ```

tests: []
file_path: functions/finance/write_ohlcv_to_parquet.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: fb5845b39cbbd0adc152d527aa7403692a7a415af9d734624a8eb56967728c0f
documentation:
  # write_ohlcv_to_parquet

  Stub para persistir datos OHLCV en formato Parquet. Pendiente de implementacion.

code:
  package finance

  import "fmt"

  // WriteOHLCVToParquet persiste datos OHLCV en un archivo Parquet.
  // TODO: implementar escritura real usando libreria Parquet.
  func WriteOHLCVToParquet(path string, data [][]float64) error {
  	return fmt.Errorf("not implemented: WriteOHLCVToParquet(%s)", path)
  }

params_schema:
  {"params":[{"name":"path","desc":"ruta destino del archivo Parquet (ej: '/data/btc_1h.parquet')"},{"name":"data","desc":"slice de slices de 5 floats en orden [open, high, low, close, volume] por cada vela"}],"output":"error si la escritura falla, nil si éxito"}


id: cache_to_sqlite_go_infra
name: cache_to_sqlite
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func CacheToSQLite(dbPath, namespace string) (*SQLiteCache, error)
tags: ["cache","sqlite","persistence","ttl","key-value","concurrent"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql","encoding/json","sync","time","fmt"]
tests: ["Set/Get basico","TTL expirado","GetOrSet con factory","Concurrencia (goroutines)"]
test_file_path: functions/infra/cache_to_sqlite_test.go
file_path: functions/infra/cache_to_sqlite.go
created_at: 2026-04-05T14:43:57Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: bd1ec3c0ffc101bef16de895943a03fa52ed5e851bb02f52bd2d40cc6d5c92db
description:
  Cache key-value persistido en SQLite con TTL y lazy eviction. Valores almacenados como JSON bytes; el caller serializa y deserializa. Thread-safe con sync.Mutex. Soporta Get, Set, Delete, Clear y GetOrSet.

example:
  ```go
  cache, err := infra.CacheToSQLite("my_cache.db", "default")
  if err != nil {
      log.Fatal(err)
  }
  defer cache.Close()

  // Almacenar JSON bytes con TTL de 1 hora
  payload, _ := json.Marshal(map[string]string{"result": "ok"})
  cache.Set("key1", payload, time.Hour)

  // Recuperar
  if v, ok := cache.Get("key1"); ok {
      var result map[string]string
      json.Unmarshal(v, &result)
      fmt.Println(result["result"]) // ok
  }

  // Factory pattern
  val, err := cache.GetOrSet("expensive_key", func() ([]byte, error) {
      return json.Marshal(computeExpensiveThing())
  }, time.Hour)

  // Helper para serializar directamente
  cache.SetJSON("user:42", userStruct, 30*time.Minute)
  ```

notes:
  Usa WAL mode para mejor concurrencia de lecturas. La eviction lazy elimina expirados en cada `Get`. El schema comparte la tabla `cache` con `cache_to_sqlite_py_infra` — ambas implementaciones son interoperables sobre el mismo archivo SQLite si usan namespaces distintos. Requiere `github.com/mattn/go-sqlite3` (ya presente en el registry).

code:
  package infra

  import (
  	"database/sql"
  	"encoding/json"
  	"fmt"
  	"sync"
  	"time"

  	_ "github.com/mattn/go-sqlite3"
  )

  // SQLiteCache es un cache key-value persistido en SQLite con soporte de TTL.
  // Valores almacenados como JSON serializado. El caller es responsable de
  // deserializar el []byte retornado por Get.
  // Seguro para uso concurrente.
  type SQLiteCache struct {
  	db        *sql.DB
  	namespace string
  	mu        sync.Mutex
  }

  const sqliteCacheSchema = `
  CREATE TABLE IF NOT EXISTS cache (
      namespace TEXT NOT NULL,
      key       TEXT NOT NULL,
      value     TEXT NOT NULL,
      created_at REAL NOT NULL,
      expires_at REAL,
      PRIMARY KEY (namespace, key)
  );`

  // CacheToSQLite abre (o crea) una base de datos SQLite en dbPath y retorna
  // un SQLiteCache para el namespace dado.
  func CacheToSQLite(dbPath, namespace string) (*SQLiteCache, error) {
  	db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL")
  	if err != nil {
  		return nil, fmt.Errorf("cache_to_sqlite: open db: %w", err)
  	}
  	if _, err := db.Exec(sqliteCacheSchema); err != nil {
  		db.Close()
  		return nil, fmt.Errorf("cache_to_sqlite: create schema: %w", err)
  	}
  	return &SQLiteCache{db: db, namespace: namespace}, nil
  }

  // evictExpired elimina las entradas expiradas del namespace. Debe llamarse
  // con el mutex ya tomado.
  func (c *SQLiteCache) evictExpired() {
  	now := float64(time.Now().UnixNano()) / 1e9
  	c.db.Exec(
  		"DELETE FROM cache WHERE namespace = ? AND expires_at IS NOT NULL AND expires_at <= ?",
  		c.namespace, now,
  	)
  }

  // Get retorna el valor asociado a key y true, o nil y false si no existe o
  // esta expirado. El []byte contiene JSON que el caller puede deserializar.
  func (c *SQLiteCache) Get(key string) ([]byte, bool) {
  	c.mu.Lock()
  	defer c.mu.Unlock()
  	c.evictExpired()
  	var value string
  	err := c.db.QueryRow(
  		"SELECT value FROM cache WHERE namespace = ? AND key = ?",
  		c.namespace, key,
  	).Scan(&value)
  	if err != nil {
  		return nil, false
  	}
  	return []byte(value), true
  }

  // Set almacena value (JSON bytes) bajo key. ttl=0 significa sin expiracion.
  func (c *SQLiteCache) Set(key string, value []byte, ttl time.Duration) error {
  	c.mu.Lock()
  	defer c.mu.Unlock()
  	now := float64(time.Now().UnixNano()) / 1e9
  	var expiresAt any
  	if ttl > 0 {
  		expiresAt = now + ttl.Seconds()
  	}
  	_, err := c.db.Exec(
  		`INSERT INTO cache (namespace, key, value, created_at, expires_at)
  		 VALUES (?, ?, ?, ?, ?)
  		 ON CONFLICT(namespace, key) DO UPDATE SET
  		     value = excluded.value,
  		     created_at = excluded.created_at,
  		     expires_at = excluded.expires_at`,
  		c.namespace, key, string(value), now, expiresAt,
  	)
  	if err != nil {
  		return fmt.Errorf("cache set: %w", err)
  	}
  	return nil
  }

  // Delete elimina la entrada asociada a key. Retorna error si falla la query.
  func (c *SQLiteCache) Delete(key string) error {
  	c.mu.Lock()
  	defer c.mu.Unlock()
  	_, err := c.db.Exec(
  		"DELETE FROM cache WHERE namespace = ? AND key = ?",
  		c.namespace, key,
  	)
  	if err != nil {
  		return fmt.Errorf("cache delete: %w", err)
  	}
  	return nil
  }

  // Clear elimina todas las entradas del namespace. Retorna el numero de filas
  // eliminadas.
  func (c *SQLiteCache) Clear() (int64, error) {
  	c.mu.Lock()
  	defer c.mu.Unlock()
  	res, err := c.db.Exec(
  		"DELETE FROM cache WHERE namespace = ?",
  		c.namespace,
  	)
  	if err != nil {
  		return 0, fmt.Errorf("cache clear: %w", err)
  	}
  	n, _ := res.RowsAffected()
  	return n, nil
  }

  // GetOrSet retorna el valor cacheado o llama factory() para obtenerlo,
  // lo almacena con el ttl dado y lo retorna.
  func (c *SQLiteCache) GetOrSet(key string, factory func() ([]byte, error), ttl time.Duration) ([]byte, error) {
  	if v, ok := c.Get(key); ok {
  		return v, nil
  	}
  	value, err := factory()
  	if err != nil {
  		return nil, fmt.Errorf("cache get_or_set factory: %w", err)
  	}
  	if err := c.Set(key, value, ttl); err != nil {
  		return nil, err
  	}
  	return value, nil
  }

  // SetJSON serializa v como JSON y lo almacena bajo key.
  func (c *SQLiteCache) SetJSON(key string, v any, ttl time.Duration) error {
  	b, err := json.Marshal(v)
  	if err != nil {
  		return fmt.Errorf("cache set_json marshal: %w", err)
  	}
  	return c.Set(key, b, ttl)
  }

  // Close cierra la conexion a la base de datos.
  func (c *SQLiteCache) Close() error {
  	return c.db.Close()
  }

params_schema:
  {"params":[{"name":"dbPath","desc":"ruta del archivo SQLite donde persiste el cache"},{"name":"namespace","desc":"prefijo de tabla para aislar caches en el mismo archivo"}],"output":"instancia thread-safe de SQLiteCache con metodos Get, Set, Delete, Clear, GetOrSet"}


id: clickhouse_open_go_infra
name: clickhouse_open
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func ClickHouseOpen(host string, port int, user, password, database string) (*sql.DB, error)
description: Conecta a ClickHouse construyendo DSN clickhouse://user:pass@host:port/database.
tags: ["database","clickhouse","connection","sql","olap"]
uses_functions: []
uses_types: ["db_config_go_infra"]
returns: []
error_type: error_go_core
imports: ["database/sql","github.com/ClickHouse/clickhouse-go/v2"]
tests: []
file_path: functions/infra/clickhouse_open.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 607bdecae65fd707f574cf7208cf80d997f87bf87cf94ad1a8535ad83f20bb90
example:
  ```go
  db, err := ClickHouseOpen("localhost", 9000, "default", "", "analytics")
  if err != nil {
      log.Fatal(err)
  }
  defer DBClose(db)

  rows, err := DBQuery(db, "SELECT event, count() FROM events GROUP BY event")
  ```

notes:
  Usa el driver `github.com/ClickHouse/clickhouse-go/v2` registrado como "clickhouse". Puerto por defecto de ClickHouse es 9000 (nativo) o 8123 (HTTP). Hace ping al abrir para verificar conectividad.

code:
  //go:build !noclickhouse

  package infra

  import (
  	"database/sql"
  	"fmt"

  	_ "github.com/ClickHouse/clickhouse-go/v2"
  )

  // ClickHouseOpen connects to a ClickHouse server and returns a *sql.DB.
  // Constructs a DSN of the form:
  //
  //	clickhouse://user:password@host:port/database
  func ClickHouseOpen(host string, port int, user, password, database string) (*sql.DB, error) {
  	dsn := fmt.Sprintf("clickhouse://%s:%s@%s:%d/%s", user, password, host, port, database)
  	db, err := sql.Open("clickhouse", dsn)
  	if err != nil {
  		return nil, fmt.Errorf("clickhouse_open: open: %w", err)
  	}
  	if err := db.Ping(); err != nil {
  		db.Close()
  		return nil, fmt.Errorf("clickhouse_open: ping %s:%d/%s: %w", host, port, database, err)
  	}
  	return db, nil
  }

params_schema:
  {"params":[{"name":"host","desc":"hostname del servidor ClickHouse"},{"name":"port","desc":"puerto de ClickHouse (9000 para protocolo nativo, 8123 para HTTP)"},{"name":"user","desc":"nombre de usuario para autenticacion"},{"name":"password","desc":"contraseña para autenticacion"},{"name":"database","desc":"nombre de la base de datos a conectar"}],"output":"conexion sql.DB abierta a ClickHouse con ping verificado"}


id: cron_ticker_go_infra
name: cron_ticker
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func CronTicker(schedule CronTickerSchedule, ctx context.Context) <-chan time.Time
tags: ["cron","scheduling","ticker","channel","goroutine","concurrency","impure"]
uses_functions: ["parse_cron_expr_go_core","next_cron_time_go_core"]
uses_types: ["cron_schedule_go_core"]
returns: []
error_type: error_go_core
imports: ["context","time"]
tests: ["context cancel cierra el channel","ticker emite al llegar el momento del schedule"]
test_file_path: functions/infra/cron_ticker_test.go
file_path: functions/infra/cron_ticker.go
created_at: 2026-04-05T11:25:56Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 0aaf82eae848d998f68d0c803c1f51cdc7d3e7bb842aa6f75e3795dae5535cb2
description:
  Crea un channel que emite time.Time en cada tick del cron schedule. Usa time.NewTimer internamente, recalculando el proximo tick tras cada emision. El channel se cierra al cancelar el context. Incluye CronTickerSchedule (reflejo local de CronSchedule para evitar dependencia cross-package).

example:
  ```go
  sched := CronTickerSchedule{
      Minute:     []int{0, 15, 30, 45},
      Hour:       intRange(0, 23),
      DayOfMonth: intRange(1, 31),
      Month:      intRange(1, 12),
      DayOfWeek:  intRange(0, 6),
  }
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()

  for tick := range CronTicker(sched, ctx) {
      fmt.Println("tick:", tick)
  }
  ```

notes:
  Funcion impura: lanza una goroutine, usa time.NewTimer y context. El tipo CronTickerSchedule es un reflejo local de core.CronSchedule para evitar imports cross-package entre dominios Go. En uso real, convertir el resultado de core.ParseCronExpr manualmente. El channel tiene buffer de 1 para evitar bloqueos si el consumidor es lento; los ticks extras se descartan.

code:
  package infra

  import (
  	"context"
  	"time"
  )

  // cronSchedule mirrors core.CronSchedule to avoid cross-package import.
  // In practice, callers should use core.ParseCronExpr and pass the result here.
  // The struct is duplicated to respect the registry rule of no cross-domain imports
  // between function packages.
  //
  // CronTickerSchedule is the schedule consumed by CronTicker.
  type CronTickerSchedule struct {
  	Minute     []int
  	Hour       []int
  	DayOfMonth []int
  	Month      []int
  	DayOfWeek  []int
  }

  // CronTicker creates a channel that emits the current time whenever the given
  // schedule fires. It uses time.NewTimer internally, recalculating the next tick
  // after each emission. The channel is closed when ctx is cancelled.
  func CronTicker(schedule CronTickerSchedule, ctx context.Context) <-chan time.Time {
  	ch := make(chan time.Time, 1)
  	go func() {
  		defer close(ch)
  		for {
  			next := cronTickerNext(schedule, time.Now())
  			if next.IsZero() {
  				// Impossible schedule — nothing to emit.
  				return
  			}
  			delay := time.Until(next)
  			timer := time.NewTimer(delay)
  			select {
  			case <-ctx.Done():
  				timer.Stop()
  				return
  			case tick := <-timer.C:
  				select {
  				case ch <- tick:
  				default:
  					// Drop if consumer is not ready.
  				}
  			}
  		}
  	}()
  	return ch
  }

  // cronTickerNext finds the next time after `after` that satisfies the schedule.
  // Returns zero time if no match within 366 days.
  func cronTickerNext(s CronTickerSchedule, after time.Time) time.Time {
  	t := after.Truncate(time.Minute).Add(time.Minute)
  	limit := after.Add(366 * 24 * time.Hour)

  	for t.Before(limit) {
  		if !cronIntIn(int(t.Month()), s.Month) {
  			t = cronNextMonth(t, s.Month)
  			if t.IsZero() {
  				return time.Time{}
  			}
  			continue
  		}
  		domOK := cronIntIn(t.Day(), s.DayOfMonth)
  		dowOK := cronIntIn(int(t.Weekday()), s.DayOfWeek)
  		if !domOK || !dowOK {
  			t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
  			continue
  		}
  		if !cronIntIn(t.Hour(), s.Hour) {
  			next := cronNextHour(t, s.Hour)
  			if next.IsZero() {
  				t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, t.Location())
  			} else {
  				t = next
  			}
  			continue
  		}
  		if !cronIntIn(t.Minute(), s.Minute) {
  			next := cronNextMinute(t, s.Minute)
  			if next.IsZero() {
  				t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, 0, 0, 0, t.Location())
  			} else {
  				t = next
  			}
  			continue
  		}
  		return t
  	}
  	return time.Time{}
  }

  func cronIntIn(v int, s []int) bool {
  	for _, x := range s {
  		if x == v {
  			return true
  		}
  	}
  	return false
  }

  func cronNextMonth(t time.Time, months []int) time.Time {
  	month := int(t.Month())
  	for _, m := range months {
  		if m > month {
  			return time.Date(t.Year(), time.Month(m), 1, 0, 0, 0, 0, t.Location())
  		}
  	}
  	if len(months) > 0 {
  		return time.Date(t.Year()+1, time.Month(months[0]), 1, 0, 0, 0, 0, t.Location())
  	}
  	return time.Time{}
  }

  func cronNextHour(t time.Time, hours []int) time.Time {
  	h := t.Hour()
  	for _, hh := range hours {
  		if hh > h {
  			return time.Date(t.Year(), t.Month(), t.Day(), hh, 0, 0, 0, t.Location())
  		}
  	}
  	return time.Time{}
  }

  func cronNextMinute(t time.Time, minutes []int) time.Time {
  	m := t.Minute()
  	for _, mm := range minutes {
  		if mm > m {
  			return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), mm, 0, 0, t.Location())
  		}
  	}
  	return time.Time{}
  }

params_schema:
  {"params":[{"name":"schedule","desc":"estructura CronTickerSchedule con campos Minute, Hour, DayOfMonth, Month, DayOfWeek"},{"name":"ctx","desc":"context que al cancelarse cierra el channel de ticks"}],"output":"channel que emite time.Time en cada tick del schedule cron"}


id: db_close_go_infra
name: db_close
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DBClose(db *sql.DB) error
tags: ["database","sql","close","lifecycle"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql"]
example:
  ```go
  db, err := SQLiteOpen("/data/app.db")
  if err != nil {
  return err
  }
  defer DBClose(db)
  ```

tests: []
file_path: functions/infra/db_close.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ff34b9a5b737ce67d86b80b83d268d7a1c52fe170e6c75f800f5a8194ad921c1
description:
  Cierra la conexion a la base de datos. Wrapper sobre db.Close() para composabilidad en pipelines que gestionan el ciclo de vida de *sql.DB explicitamente.

notes:
  Retorna error si db es nil. En la mayoria de los casos se usa con `defer`. Existe como funcion del registry para que los pipelines puedan referenciarla en `uses_functions` y modelar el ciclo de vida completo de la conexion.

code:
  package infra

  import (
  	"database/sql"
  	"fmt"
  )

  // DBClose closes the database connection. Wraps db.Close() for composability
  // in pipelines that manage *sql.DB lifecycle explicitly.
  func DBClose(db *sql.DB) error {
  	if db == nil {
  		return fmt.Errorf("db_close: db is nil")
  	}
  	if err := db.Close(); err != nil {
  		return fmt.Errorf("db_close: %w", err)
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"db","desc":"conexion sql.DB a cerrar"}],"output":"nil si el cierre fue exitoso, error en caso contrario"}


id: db_create_table_go_infra
name: db_create_table
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DBCreateTable(db *sql.DB, table string, columns []string) error
tags: ["database","sql","ddl","create","table","schema"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql","regexp","strings"]
tests: []
file_path: functions/infra/db_create_table.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 2a0e3c233a4a2135d09393f437192778412c9dccf2d93e1a57587281c1d957ea
description:
  Ejecuta CREATE TABLE IF NOT EXISTS con las definiciones de columnas dadas. Valida que el nombre de tabla sea un identificador SQL seguro.

example:
  ```go
  err := DBCreateTable(db, "events", []string{
      "id INTEGER PRIMARY KEY AUTOINCREMENT",
      "name TEXT NOT NULL",
      "ts  INTEGER NOT NULL",
      "payload TEXT",
  })
  ```

notes:
  `columns` son definiciones SQL completas incluyendo nombre, tipo y constraints. Usa `CREATE TABLE IF NOT EXISTS` para ser idempotente. Valida el nombre de tabla con regex `^[a-zA-Z_][a-zA-Z0-9_]*$` para prevenir SQL injection. Las definiciones de columna no se sanitizan — son responsabilidad del llamador.

code:
  package infra

  import (
  	"database/sql"
  	"fmt"
  	"regexp"
  	"strings"
  )

  var validIdentifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)

  // DBCreateTable executes CREATE TABLE IF NOT EXISTS for the given table with
  // the provided column definitions. Each element of columns should be a full
  // SQL column definition, e.g. "id INTEGER PRIMARY KEY" or "name TEXT NOT NULL".
  // Returns an error if the table name contains invalid characters.
  func DBCreateTable(db *sql.DB, table string, columns []string) error {
  	if !validIdentifier.MatchString(table) {
  		return fmt.Errorf("db_create_table: invalid table name %q (only alphanumeric and underscore allowed)", table)
  	}
  	if len(columns) == 0 {
  		return fmt.Errorf("db_create_table: at least one column definition required")
  	}
  	query := fmt.Sprintf(
  		"CREATE TABLE IF NOT EXISTS %s (%s)",
  		table,
  		strings.Join(columns, ", "),
  	)
  	if _, err := db.Exec(query); err != nil {
  		return fmt.Errorf("db_create_table %q: %w", table, err)
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"db","desc":"conexion sql.DB abierta"},{"name":"table","desc":"nombre de la tabla a crear"},{"name":"columns","desc":"slice de definiciones SQL completas de columnas (incluyen nombre, tipo, constraints)"}],"output":"nil si la tabla se creo exitosamente, error en caso contrario"}


id: db_exec_go_infra
name: db_exec
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DBExec(db *sql.DB, query string, args ...any) (int64, error)
tags: ["database","sql","exec","insert","update","delete","ddl"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql"]
tests: []
file_path: functions/infra/db_exec.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 82f949a4a515c14b76f03b54c43241c2f623aec483fe98207ff11761a09f8915
description:
  Ejecuta un statement no-SELECT (INSERT, UPDATE, DELETE, DDL) y retorna el numero de filas afectadas.

example:
  ```go
  n, err := DBExec(db, "UPDATE users SET active = ? WHERE last_login < ?", false, cutoff)
  if err != nil {
      return err
  }
  fmt.Printf("desactivados: %d usuarios\n", n)
  ```

notes:
  Agnóstica al driver. Para DDL algunos drivers retornan 0 en RowsAffected — esto es normal. Para INSERT con last_insert_id usar `DBInsertRow` que retorna ese valor. Para multiples filas en una transaccion usar `DBInsertBatch`.

code:
  package infra

  import (
  	"database/sql"
  	"fmt"
  )

  // DBExec executes a non-SELECT statement (INSERT, UPDATE, DELETE, DDL) and
  // returns the number of rows affected. For statements that don't return rows
  // affected (e.g. DDL on some drivers), the count may be 0.
  func DBExec(db *sql.DB, query string, args ...any) (int64, error) {
  	result, err := db.Exec(query, args...)
  	if err != nil {
  		return 0, fmt.Errorf("db_exec: %w", err)
  	}
  	n, err := result.RowsAffected()
  	if err != nil {
  		// Some drivers don't support RowsAffected; treat as 0.
  		return 0, nil
  	}
  	return n, nil
  }

params_schema:
  {"params":[{"name":"db","desc":"conexion sql.DB abierta"},{"name":"query","desc":"statement SQL no-SELECT (INSERT, UPDATE, DELETE, DDL) con placeholders ?"},{"name":"args","desc":"argumentos para los placeholders en la query"}],"output":"numero de filas afectadas por la ejecucion"}


id: db_insert_batch_go_infra
name: db_insert_batch
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DBInsertBatch(db *sql.DB, table string, columns []string, rows [][]any) (int64, error)
tags: ["database","sql","insert","batch","transaction","bulk"]
uses_functions: ["db_insert_row_go_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql","strings"]
tests: []
file_path: functions/infra/db_insert_batch.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5472d5692b3382664e7cd7c97b6496f54d0c956c4549a40bfa3bc3ac13584949
description:
  Inserta multiples filas en una transaccion usando prepared statement. Retorna el total de filas afectadas. Mas eficiente que llamar DBInsertRow en un loop.

example:
  ```go
  cols := []string{"name", "score", "ts"}
  rows := [][]any{
      {"Alice", 95.5, 1700000000},
      {"Bob",   87.2, 1700000001},
      {"Carol", 91.0, 1700000002},
  }
  n, err := DBInsertBatch(db, "results", cols, rows)
  if err != nil {
      return err
  }
  fmt.Printf("insertadas: %d filas\n", n)
  ```

notes:
  Usa `tx.Prepare()` + `stmt.Exec()` en un loop dentro de una transaccion. El rollback es automatico si alguna fila falla. Valida tabla y columnas con regex. Cada fila debe tener exactamente `len(columns)` valores — retorna error descriptivo si no coincide.

code:
  package infra

  import (
  	"database/sql"
  	"fmt"
  	"strings"
  )

  // DBInsertBatch inserts multiple rows into a table using a prepared statement
  // inside a transaction. columns must match the order of values in each row.
  // Returns the total number of rows affected.
  // Column and table names are validated to contain only safe identifier chars.
  func DBInsertBatch(db *sql.DB, table string, columns []string, rows [][]any) (int64, error) {
  	if !validIdentifier.MatchString(table) {
  		return 0, fmt.Errorf("db_insert_batch: invalid table name %q", table)
  	}
  	if len(columns) == 0 {
  		return 0, fmt.Errorf("db_insert_batch: columns must not be empty")
  	}
  	if len(rows) == 0 {
  		return 0, nil
  	}

  	for _, col := range columns {
  		if !validIdentifier.MatchString(col) {
  			return 0, fmt.Errorf("db_insert_batch: invalid column name %q", col)
  		}
  	}

  	placeholders := make([]string, len(columns))
  	for i := range columns {
  		placeholders[i] = "?"
  	}
  	query := fmt.Sprintf(
  		"INSERT INTO %s (%s) VALUES (%s)",
  		table,
  		strings.Join(columns, ", "),
  		strings.Join(placeholders, ", "),
  	)

  	tx, err := db.Begin()
  	if err != nil {
  		return 0, fmt.Errorf("db_insert_batch: begin tx: %w", err)
  	}
  	defer tx.Rollback() //nolint:errcheck

  	stmt, err := tx.Prepare(query)
  	if err != nil {
  		return 0, fmt.Errorf("db_insert_batch: prepare: %w", err)
  	}
  	defer stmt.Close()

  	var total int64
  	for i, row := range rows {
  		if len(row) != len(columns) {
  			return 0, fmt.Errorf("db_insert_batch: row %d has %d values, expected %d", i, len(row), len(columns))
  		}
  		result, err := stmt.Exec(row...)
  		if err != nil {
  			return 0, fmt.Errorf("db_insert_batch: exec row %d: %w", i, err)
  		}
  		n, _ := result.RowsAffected()
  		total += n
  	}

  	if err := tx.Commit(); err != nil {
  		return 0, fmt.Errorf("db_insert_batch: commit: %w", err)
  	}
  	return total, nil
  }

params_schema:
  {"params":[{"name":"db","desc":"conexion sql.DB abierta"},{"name":"table","desc":"nombre de la tabla donde insertar"},{"name":"columns","desc":"slice de nombres de columnas en el orden de las rows"},{"name":"rows","desc":"slice de rows, donde cada row es []any con valores en orden de columns"}],"output":"numero total de filas insertadas exitosamente"}


id: db_insert_row_go_infra
name: db_insert_row
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DBInsertRow(db *sql.DB, table string, row map[string]any) (int64, error)
tags: ["database","sql","insert","row","dynamic"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql","sort","strings"]
tests: []
file_path: functions/infra/db_insert_row.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 52110c9e52f41e8ad78b8e6f108df05690e6bbd90d0d01f57b244665caf21e67
description:
  Genera y ejecuta un INSERT de una sola fila desde un map columna→valor. Retorna el last insert ID. Sanitiza nombres de tabla y columnas.

example:
  ```go
  id, err := DBInsertRow(db, "users", map[string]any{
      "name":   "Alice",
      "email":  "alice@example.com",
      "active": true,
  })
  if err != nil {
      return err
  }
  fmt.Printf("nuevo usuario ID: %d\n", id)
  ```

notes:
  Las claves del map se ordenan alfabeticamente para generar queries deterministas. Valida tabla y columnas con regex `^[a-zA-Z_][a-zA-Z0-9_]*$`. Para insertar muchas filas usar `DBInsertBatch` que es mas eficiente. El last insert ID puede ser 0 en drivers que no lo soportan (ej: postgres — usar RETURNING en su lugar con `DBQuery`).

code:
  package infra

  import (
  	"database/sql"
  	"fmt"
  	"sort"
  	"strings"
  )

  // DBInsertRow generates and executes a single-row INSERT from a map of
  // column→value pairs. Returns the last insert ID reported by the driver.
  // Column and table names are validated to contain only safe identifier chars.
  func DBInsertRow(db *sql.DB, table string, row map[string]any) (int64, error) {
  	if !validIdentifier.MatchString(table) {
  		return 0, fmt.Errorf("db_insert_row: invalid table name %q", table)
  	}
  	if len(row) == 0 {
  		return 0, fmt.Errorf("db_insert_row: row map must not be empty")
  	}

  	// Sort keys for deterministic query generation.
  	cols := make([]string, 0, len(row))
  	for col := range row {
  		if !validIdentifier.MatchString(col) {
  			return 0, fmt.Errorf("db_insert_row: invalid column name %q", col)
  		}
  		cols = append(cols, col)
  	}
  	sort.Strings(cols)

  	placeholders := make([]string, len(cols))
  	values := make([]any, len(cols))
  	for i, col := range cols {
  		placeholders[i] = "?"
  		values[i] = row[col]
  	}

  	query := fmt.Sprintf(
  		"INSERT INTO %s (%s) VALUES (%s)",
  		table,
  		strings.Join(cols, ", "),
  		strings.Join(placeholders, ", "),
  	)

  	result, err := db.Exec(query, values...)
  	if err != nil {
  		return 0, fmt.Errorf("db_insert_row %q: %w", table, err)
  	}
  	id, err := result.LastInsertId()
  	if err != nil {
  		return 0, nil
  	}
  	return id, nil
  }

params_schema:
  {"params":[{"name":"db","desc":"conexion sql.DB abierta"},{"name":"table","desc":"nombre de la tabla donde insertar"},{"name":"row","desc":"mapa de columna → valor con los datos a insertar"}],"output":"ID autoincrementado de la fila insertada"}


id: db_query_go_infra
name: db_query
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DBQuery(db *sql.DB, query string, args ...any) ([]map[string]any, error)
tags: ["database","sql","query","select","generic"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["database/sql"]
tests: []
file_path: functions/infra/db_query.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5e062b223f6aee38b25508a85a2fabfe4a63b3125e941e306bec0b0f9d5fd4c0
description:
  Ejecuta un SELECT y retorna los resultados como slice de maps. Convierte valores a tipos nativos Go segun el tipo de columna reportado por el driver.

example:
  ```go
  rows, err := DBQuery(db, "SELECT id, name, score FROM players WHERE active = ?", true)
  if err != nil {
      return err
  }
  for _, row := range rows {
      fmt.Println(row["name"], row["score"])
  }
  ```

notes:
  Agnóstica al driver — funciona con cualquier `*sql.DB` (sqlite, duckdb, postgres, clickhouse). Usa `sql.RawBytes` + `ColumnTypes()` para conversion dinamica. Convierte INTEGER→int64, FLOAT/REAL/DOUBLE→float64, BOOLEAN→bool, BLOB→[]byte, NULL→nil, resto→string. Para queries con muchos resultados considerar paginar con LIMIT/OFFSET.

code:
  package infra

  import (
  	"database/sql"
  	"fmt"
  	"strconv"
  )

  // DBQuery executes a SELECT query and returns the results as a slice of maps.
  // Each map key is the column name; values are converted to native Go types:
  // int64, float64, bool, string, []byte, or nil for NULLs.
  func DBQuery(db *sql.DB, query string, args ...any) ([]map[string]any, error) {
  	rows, err := db.Query(query, args...)
  	if err != nil {
  		return nil, fmt.Errorf("db_query: %w", err)
  	}
  	defer rows.Close()

  	cols, err := rows.Columns()
  	if err != nil {
  		return nil, fmt.Errorf("db_query: columns: %w", err)
  	}
  	colTypes, err := rows.ColumnTypes()
  	if err != nil {
  		return nil, fmt.Errorf("db_query: column types: %w", err)
  	}

  	var results []map[string]any
  	for rows.Next() {
  		// Use RawBytes so we can inspect the raw value before converting.
  		raw := make([]sql.RawBytes, len(cols))
  		ptrs := make([]any, len(cols))
  		for i := range raw {
  			ptrs[i] = &raw[i]
  		}
  		if err := rows.Scan(ptrs...); err != nil {
  			return nil, fmt.Errorf("db_query: scan: %w", err)
  		}

  		row := make(map[string]any, len(cols))
  		for i, col := range cols {
  			if raw[i] == nil {
  				row[col] = nil
  				continue
  			}
  			row[col] = convertRaw(raw[i], colTypes[i].DatabaseTypeName())
  		}
  		results = append(results, row)
  	}
  	if err := rows.Err(); err != nil {
  		return nil, fmt.Errorf("db_query: rows: %w", err)
  	}
  	return results, nil
  }

  // convertRaw attempts to convert a raw SQL byte slice into a native Go type
  // based on the database column type name hint.
  func convertRaw(b sql.RawBytes, dbType string) any {
  	s := string(b)
  	switch dbType {
  	case "INTEGER", "INT", "BIGINT", "SMALLINT", "TINYINT", "INT2", "INT4", "INT8":
  		if v, err := strconv.ParseInt(s, 10, 64); err == nil {
  			return v
  		}
  	case "REAL", "FLOAT", "DOUBLE", "NUMERIC", "DECIMAL":
  		if v, err := strconv.ParseFloat(s, 64); err == nil {
  			return v
  		}
  	case "BOOLEAN", "BOOL":
  		if v, err := strconv.ParseBool(s); err == nil {
  			return v
  		}
  	case "BLOB":
  		cp := make([]byte, len(b))
  		copy(cp, b)
  		return cp
  	}
  	return s
  }

params_schema:
  {"params":[{"name":"db","desc":"conexion sql.DB abierta"},{"name":"query","desc":"statement SELECT con placeholders ?"},{"name":"args","desc":"argumentos para los placeholders en la query"}],"output":"slice de maps donde cada map es una fila con columna → valor"}


id: deploy_app_go_infra
name: deploy_app
kind: pipeline
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["docker","deploy","go","pipeline","infra","container"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/deploy_app.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5fbf60b4130d0377ed3fcff1eeb034bd936d664e3cc5a554e39681ce2ecb1d5d
signature:
  func DeployApp(appDir string, imageName string, port int, envVars map[string]string) (string, error)

description:
  Orquesta el deploy completo de una app Go en Docker. Pasos: genera Dockerfile, lo escribe a disco, construye la imagen y lanza el contenedor en modo detach con port mapping. Retorna el container ID.

uses_functions:
  ["generate_dockerfile_go_infra","write_dockerfile_go_infra","docker_build_image_go_infra","docker_run_container_go_infra"]

example:
  ```go
  containerID, err := DeployApp(
      "/home/user/apps/myapp",
      "myapp",
      8080,
      map[string]string{
          "DB_HOST": "postgres",
          "PORT":    "8080",
      },
  )
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("Contenedor lanzado:", containerID)
  ```

notes:
  Pipeline de 4 pasos: generate_dockerfile (pura) → write_dockerfile → docker_build_image → docker_run_container. El nombre del contenedor e imagen coinciden con imageName. El port mapping es simetrico (hostPort == containerPort). Si cualquier paso falla, el pipeline retorna error con contexto del paso fallido. No hace rollback automatico — para limpiar usar stop_app.

code:
  package infra

  import (
  	"fmt"
  )

  // DeployApp orquesta el deploy completo de una app Go en Docker.
  // Pasos: genera Dockerfile → lo escribe → build image → run container (detach, port mapping).
  // Retorna el container ID del contenedor lanzado.
  func DeployApp(appDir string, imageName string, port int, envVars map[string]string) (string, error) {
  	// 1. Generar Dockerfile (puro)
  	content := GenerateDockerfile(imageName, port, envVars)

  	// 2. Escribir Dockerfile a disco
  	_, err := WriteDockerfile(appDir, content)
  	if err != nil {
  		return "", fmt.Errorf("deploy_app: escribir Dockerfile: %w", err)
  	}

  	// 3. Build de la imagen Docker
  	tag := imageName + ":latest"
  	_, err = DockerBuildImage(appDir, tag, nil)
  	if err != nil {
  		return "", fmt.Errorf("deploy_app: build imagen %s: %w", tag, err)
  	}

  	// 4. Ejecutar el contenedor en modo detach con port mapping
  	portMapping := fmt.Sprintf("%d:%d", port, port)
  	opts := DockerRunOpts{
  		Name:   imageName,
  		Ports:  []string{portMapping},
  		Env:    envVars,
  		Detach: true,
  	}
  	containerID, err := DockerRunContainer(tag, opts)
  	if err != nil {
  		return "", fmt.Errorf("deploy_app: run contenedor %s: %w", imageName, err)
  	}

  	return containerID, nil
  }

params_schema:
  {"params":[{"name":"appDir","desc":"ruta del directorio de la app Go (contiene main.go)"},{"name":"imageName","desc":"nombre de la imagen Docker a construir y contenedor a lanzar"},{"name":"port","desc":"puerto a mapear (hostPort == containerPort)"},{"name":"envVars","desc":"mapa de variables de entorno a pasar al contenedor"}],"output":"ID del contenedor Docker lanzado exitosamente"}


id: docker_build_image_go_infra
name: docker_build_image
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerBuildImage(contextDir, tag string, buildArgs map[string]string) (string, error)
tags: ["docker","image","build","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
test_file_path: functions/infra/docker_build_image_test.go
file_path: functions/infra/docker_build_image.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ab362ea2853b84cde2af55fb49def55bd62f0cea972c73c77eb5312567044463
description:
  Construye una imagen Docker desde un directorio con Dockerfile. Soporta build args opcionales. Retorna el image ID de la imagen construida.

example:
  ```go
  imageID, err := DockerBuildImage("./myapp", "myapp:latest", map[string]string{
      "VERSION": "1.2.3",
      "ENV":     "production",
  })
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("Built:", imageID)
  ```

tests:
  ["build sin build args retorna image ID","build con build args incluye --build-arg","error si contextDir no existe"]

notes:
  Ejecuta `docker build -t tag contextDir --build-arg key=val ...`. Parsea el image ID del output de docker build, compatible con el builder clasico (mensajes "Successfully built") y BuildKit (sha256). Si no puede parsear el ID del output, hace un `docker inspect` por tag como fallback.

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerBuildImage construye una imagen Docker desde un directorio con Dockerfile.
  // Retorna el image ID de la imagen construida.
  func DockerBuildImage(contextDir, tag string, buildArgs map[string]string) (string, error) {
  	args := []string{"build", "-t", tag}

  	for k, v := range buildArgs {
  		args = append(args, "--build-arg", k+"="+v)
  	}

  	args = append(args, contextDir)

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker build %s: %s", tag, strings.TrimSpace(string(out)))
  	}

  	// Extraer el image ID de la ultima linea de output
  	// docker build imprime "Successfully built <id>" o con BuildKit "writing image sha256:<id>"
  	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
  	for i := len(lines) - 1; i >= 0; i-- {
  		line := strings.TrimSpace(lines[i])
  		if strings.Contains(line, "Successfully built ") {
  			parts := strings.Fields(line)
  			return parts[len(parts)-1], nil
  		}
  		if strings.Contains(line, "sha256:") {
  			// BuildKit: extraer sha256:...
  			idx := strings.Index(line, "sha256:")
  			id := line[idx:]
  			if sp := strings.IndexAny(id, " \t,"); sp != -1 {
  				id = id[:sp]
  			}
  			return id, nil
  		}
  	}

  	// Si no encontramos el ID en el output, inspeccionar por tag
  	inspectOut, err2 := exec.Command("docker", "inspect", "--format", "{{.Id}}", tag).Output()
  	if err2 != nil {
  		return tag, nil
  	}
  	return strings.TrimSpace(string(inspectOut)), nil
  }

params_schema:
  {"params":[{"name":"contextDir","desc":"ruta del directorio que contiene el Dockerfile"},{"name":"tag","desc":"nombre y tag de la imagen a construir (ej: myapp:latest)"},{"name":"buildArgs","desc":"mapa de argumentos de construccion (--build-arg key=val)"}],"output":"ID de la imagen Docker construida"}


id: docker_compose_down_go_infra
name: docker_compose_down
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerComposeDown(composeFile string, removeVolumes bool) (string, error)
tags: ["docker","compose","down","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
tests: ["removeVolumes true agrega flag -v al comando","error si composeFile no existe"]
test_file_path: functions/infra/docker_compose_down_test.go
file_path: functions/infra/docker_compose_down.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 6bccfd2f500303b54575973cbb2e57bdbb4366f25c65becb7b51a9e20c90e415
description:
  Baja un stack docker-compose desde el archivo dado. Si removeVolumes es true elimina también los volumes declarados (-v). Retorna el stdout del comando.

example:
  ```go
  // Bajar stack y limpiar volumes
  out, err := DockerComposeDown("./docker-compose.yml", true)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(out)
  ```

notes:
  Ejecuta `docker compose -f composeFile down [-v]`. Usa el subcomando `docker compose` (V2). El flag -v elimina volumes nombrados declarados en el compose file, no volumes externos. Retorna stdout + stderr combinados.

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerComposeDown baja un stack docker-compose desde el archivo dado.
  // Si removeVolumes es true elimina también los volumes (-v). Retorna el stdout del comando.
  func DockerComposeDown(composeFile string, removeVolumes bool) (string, error) {
  	args := []string{"compose", "-f", composeFile, "down"}
  	if removeVolumes {
  		args = append(args, "-v")
  	}

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker compose down %s: %s", composeFile, strings.TrimSpace(string(out)))
  	}
  	return strings.TrimSpace(string(out)), nil
  }

params_schema:
  {"params":[{"name":"composeFile","desc":"ruta del archivo docker-compose.yml"},{"name":"removeVolumes","desc":"si true elimina tambien los volumes declarados"}],"output":"stdout combinado con stderr de la ejecucion de docker compose down"}


id: docker_compose_up_go_infra
name: docker_compose_up
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerComposeUp(composeFile string, detach bool) (string, error)
tags: ["docker","compose","up","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
tests: ["detach true agrega flag -d al comando","error si composeFile no existe"]
test_file_path: functions/infra/docker_compose_up_test.go
file_path: functions/infra/docker_compose_up.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7cc3da201773c152d57855bb36e96bbd67749cd7a68e02133d40180e6e32fbf5
description:
  Levanta un stack docker-compose desde el archivo dado. Si detach es true ejecuta en background (-d). Retorna el stdout del comando.

example:
  ```go
  out, err := DockerComposeUp("./docker-compose.yml", true)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(out)
  ```

notes:
  Ejecuta `docker compose -f composeFile up [-d]`. Usa el subcomando `docker compose` (V2), no el binario standalone `docker-compose`. Retorna stdout + stderr combinados para facilitar el debugging. En modo no-detach bloquea hasta que el compose termine (util para stacks efimeros).

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerComposeUp levanta un stack docker-compose desde el archivo dado.
  // Si detach es true ejecuta en background (-d). Retorna el stdout del comando.
  func DockerComposeUp(composeFile string, detach bool) (string, error) {
  	args := []string{"compose", "-f", composeFile, "up"}
  	if detach {
  		args = append(args, "-d")
  	}

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker compose up %s: %s", composeFile, strings.TrimSpace(string(out)))
  	}
  	return strings.TrimSpace(string(out)), nil
  }

params_schema:
  {"params":[{"name":"composeFile","desc":"ruta del archivo docker-compose.yml"},{"name":"detach","desc":"si true ejecuta en background sin bloquear"}],"output":"stdout combinado con stderr de la ejecucion de docker compose up"}


id: docker_container_logs_go_infra
name: docker_container_logs
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerContainerLogs(nameOrID string, tail int) (string, error)
tags: ["docker","container","logs","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strconv"]
tests: []
file_path: functions/infra/docker_container_logs.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1e5d36a978b169ef8f4d55e551569ea3392e5919bc977b650e65c30d9292be6e
description:
  Obtiene los logs de un contenedor Docker. El parámetro tail limita a las últimas N líneas (0 devuelve todos los logs).

example:
  ```go
  // Últimas 100 líneas
  logs, err := DockerContainerLogs("my-app", 100)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(logs)
  ```

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strconv"
  )

  // DockerContainerLogs obtiene los logs de un contenedor. tail limita las últimas N líneas (0 = todas).
  func DockerContainerLogs(nameOrID string, tail int) (string, error) {
  	args := []string{"logs"}
  	if tail > 0 {
  		args = append(args, "--tail", strconv.Itoa(tail))
  	}
  	args = append(args, nameOrID)

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker logs %s: %w", nameOrID, err)
  	}

  	return string(out), nil
  }

params_schema:
  {"params":[{"name":"nameOrID","desc":"nombre o ID del contenedor Docker"},{"name":"tail","desc":"numero de ultimas lineas a devolver (0 devuelve todos los logs)"}],"output":"logs del contenedor como string"}


id: docker_create_network_go_infra
name: docker_create_network
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerCreateNetwork(name, driver string) (string, error)
tags: ["docker","network","create","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
tests: []
file_path: functions/infra/docker_create_network.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: cc9aca42c3939db3cf8b198eeabe003623c770b9d09881a0490366ed997b5804
description:
  Crea una red Docker con el nombre y driver dados. Si driver está vacío usa bridge por defecto. Devuelve el ID de la red creada.

example:
  ```go
  netID, err := DockerCreateNetwork("my-app-net", "bridge")
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("Network ID:", netID)
  ```

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerCreateNetwork crea una red Docker con el nombre y driver dados.
  // Si el driver está vacío, usa "bridge" por defecto.
  // Devuelve el ID de la red creada.
  func DockerCreateNetwork(name, driver string) (string, error) {
  	if name == "" {
  		return "", fmt.Errorf("network name required")
  	}
  	if driver == "" {
  		driver = "bridge"
  	}

  	out, err := exec.Command("docker", "network", "create", "--driver", driver, name).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker network create %s: %s", name, strings.TrimSpace(string(out)))
  	}

  	return strings.TrimSpace(string(out)), nil
  }

params_schema:
  {"params":[{"name":"name","desc":"nombre de la red Docker a crear"},{"name":"driver","desc":"driver de la red (bridge, host, overlay, etc. o vacio para bridge por defecto)"}],"output":"ID de la red Docker creada"}


id: docker_inspect_container_go_infra
name: docker_inspect_container
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerInspectContainer(nameOrID string) (map[string]any, error)
tags: ["docker","container","inspect","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["encoding/json","fmt","os/exec"]
tests: []
file_path: functions/infra/docker_inspect_container.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 743a3fa9527774b2f1bb1432e03c16bbbae822f338ce46c81cd5a3446861b2d5
description:
  Devuelve los detalles completos de un contenedor Docker como mapa JSON genérico. Útil para inspeccionar configuración, red, volumes, etc.

example:
  ```go
  info, err := DockerInspectContainer("my-app")
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(info["State"])
  ```

notes:
  Devuelve `map[string]any` en vez de un struct tipado para maximizar flexibilidad. El caller puede navegar el JSON libremente.

code:
  package infra

  import (
  	"encoding/json"
  	"fmt"
  	"os/exec"
  )

  // DockerInspectContainer devuelve los detalles completos de un contenedor como JSON genérico.
  func DockerInspectContainer(nameOrID string) (map[string]any, error) {
  	out, err := exec.Command("docker", "inspect", nameOrID).Output()
  	if err != nil {
  		return nil, fmt.Errorf("docker inspect %s: %w", nameOrID, err)
  	}

  	var result []map[string]any
  	if err := json.Unmarshal(out, &result); err != nil {
  		return nil, fmt.Errorf("parsing inspect output: %w", err)
  	}

  	if len(result) == 0 {
  		return nil, fmt.Errorf("container %s not found", nameOrID)
  	}

  	return result[0], nil
  }

params_schema:
  {"params":[{"name":"nameOrID","desc":"nombre o ID del contenedor Docker a inspeccionar"}],"output":"mapa generico con detalles completos del contenedor (estado, red, volumes, etc.)"}


id: docker_list_containers_go_infra
name: docker_list_containers
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerListContainers(all bool) ([]ContainerInfo, error)
tags: ["docker","container","list","infra"]
uses_functions: []
uses_types: ["container_info_go_infra"]
returns: ["container_info_go_infra"]
error_type: error_go_core
imports: ["encoding/json","fmt","os/exec"]
tests: []
file_path: functions/infra/docker_list_containers.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1c0d3edc23d26019cdbf99f84527d1c39547f11079ad1b86bf374f5db0321e54
description:
  Lista contenedores Docker locales. Si all es true incluye contenedores detenidos. Parsea la salida JSON de docker ps.

example:
  ```go
  containers, err := DockerListContainers(true)
  if err != nil {
      log.Fatal(err)
  }
  for _, c := range containers {
      fmt.Printf("%s %s %s\n", c.ID, c.Name, c.Status)
  }
  ```

notes:
  Usa `docker ps --format '{{json .}}'` para parsear la salida de forma confiable. Incluye helpers internos `splitLines` y `parseLabels`.

code:
  package infra

  import (
  	"encoding/json"
  	"fmt"
  	"os/exec"
  )

  // DockerListContainers lista contenedores Docker. Si all es true, incluye los detenidos.
  func DockerListContainers(all bool) ([]ContainerInfo, error) {
  	args := []string{"ps", "--format", "{{json .}}", "--no-trunc"}
  	if all {
  		args = append(args, "-a")
  	}

  	out, err := exec.Command("docker", args...).Output()
  	if err != nil {
  		return nil, fmt.Errorf("docker ps: %w", err)
  	}

  	if len(out) == 0 {
  		return nil, nil
  	}

  	var containers []ContainerInfo
  	for _, line := range splitLines(out) {
  		if len(line) == 0 {
  			continue
  		}
  		var raw struct {
  			ID      string `json:"ID"`
  			Names   string `json:"Names"`
  			Image   string `json:"Image"`
  			Status  string `json:"Status"`
  			State   string `json:"State"`
  			Ports   string `json:"Ports"`
  			Created string `json:"CreatedAt"`
  			Labels  string `json:"Labels"`
  		}
  		if err := json.Unmarshal(line, &raw); err != nil {
  			continue
  		}
  		containers = append(containers, ContainerInfo{
  			ID:      raw.ID[:12],
  			Name:    raw.Names,
  			Image:   raw.Image,
  			Status:  raw.Status,
  			State:   raw.State,
  			Ports:   raw.Ports,
  			Created: raw.Created,
  			Labels:  parseLabels(raw.Labels),
  		})
  	}

  	return containers, nil
  }

  // splitLines divide bytes en líneas individuales.
  func splitLines(data []byte) [][]byte {
  	var lines [][]byte
  	start := 0
  	for i, b := range data {
  		if b == '\n' {
  			lines = append(lines, data[start:i])
  			start = i + 1
  		}
  	}
  	if start < len(data) {
  		lines = append(lines, data[start:])
  	}
  	return lines
  }

  // parseLabels convierte "key=val,key2=val2" en map.
  func parseLabels(s string) map[string]string {
  	m := make(map[string]string)
  	if s == "" {
  		return m
  	}
  	for _, part := range splitByComma(s) {
  		for i := 0; i < len(part); i++ {
  			if part[i] == '=' {
  				m[part[:i]] = part[i+1:]
  				break
  			}
  		}
  	}
  	return m
  }

  func splitByComma(s string) []string {
  	var parts []string
  	start := 0
  	for i := 0; i < len(s); i++ {
  		if s[i] == ',' {
  			parts = append(parts, s[start:i])
  			start = i + 1
  		}
  	}
  	if start < len(s) {
  		parts = append(parts, s[start:])
  	}
  	return parts
  }

params_schema:
  {"params":[{"name":"all","desc":"si true incluye contenedores detenidos, si false solo los que estan corriendo"}],"output":"slice de ContainerInfo con ID, nombre, estado de cada contenedor"}


id: docker_list_images_go_infra
name: docker_list_images
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerListImages() ([]ImageInfo, error)
description: Lista las imágenes Docker disponibles localmente. Parsea la salida JSON de docker images.
tags: ["docker","image","list","infra"]
uses_functions: []
uses_types: ["image_info_go_infra"]
returns: ["image_info_go_infra"]
error_type: error_go_core
imports: ["encoding/json","fmt","os/exec"]
tests: []
file_path: functions/infra/docker_list_images.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 15fb2fa22ae96927f18fbe731c99d40f27acc5125aa7c76e913fa0f3b7157acc
params_schema: {"output":"slice de ImageInfo con repositorio, tag, size de cada imagen"}
example:
  ```go
  images, err := DockerListImages()
  if err != nil {
      log.Fatal(err)
  }
  for _, img := range images {
      fmt.Printf("%s:%s (%s)\n", img.Repository, img.Tag, img.Size)
  }
  ```

code:
  package infra

  import (
  	"encoding/json"
  	"fmt"
  	"os/exec"
  )

  // DockerListImages lista las imágenes Docker locales.
  func DockerListImages() ([]ImageInfo, error) {
  	out, err := exec.Command("docker", "images", "--format", "{{json .}}", "--no-trunc").Output()
  	if err != nil {
  		return nil, fmt.Errorf("docker images: %w", err)
  	}

  	if len(out) == 0 {
  		return nil, nil
  	}

  	var images []ImageInfo
  	for _, line := range splitLines(out) {
  		if len(line) == 0 {
  			continue
  		}
  		var raw struct {
  			ID         string `json:"ID"`
  			Repository string `json:"Repository"`
  			Tag        string `json:"Tag"`
  			Size       string `json:"Size"`
  			Created    string `json:"CreatedAt"`
  		}
  		if err := json.Unmarshal(line, &raw); err != nil {
  			continue
  		}
  		images = append(images, ImageInfo{
  			ID:         raw.ID,
  			Repository: raw.Repository,
  			Tag:        raw.Tag,
  			Size:       raw.Size,
  			Created:    raw.Created,
  		})
  	}

  	return images, nil
  }


id: docker_pull_image_go_infra
name: docker_pull_image
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerPullImage(image string) error
tags: ["docker","image","pull","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
example:
  ```go
  err := DockerPullImage("nginx:latest")
  if err != nil {
  log.Fatal(err)
  }
  ```

tests: []
file_path: functions/infra/docker_pull_image.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 15d3252cde4e1ddd350f7b1b51a8727f7e706fba1d5bace6314f07a10127d41d
description:
  Descarga una imagen Docker desde el registry remoto (Docker Hub u otro configurado). Acepta formato image:tag.

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerPullImage descarga una imagen Docker del registry remoto.
  func DockerPullImage(image string) error {
  	out, err := exec.Command("docker", "pull", image).CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("docker pull %s: %s", image, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"image","desc":"referencia de imagen Docker en formato image:tag (ej: nginx:latest)"}],"output":"nil si la descarga fue exitosa, error en caso contrario"}


id: docker_remove_container_go_infra
name: docker_remove_container
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerRemoveContainer(nameOrID string, force bool) error
tags: ["docker","container","remove","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
tests: []
file_path: functions/infra/docker_remove_container.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5890ff467db7478ebc48732357d58524a9c268225947f3c816b8f908a5d54fca
description:
  Elimina un contenedor Docker. Con force=true puede eliminar contenedores en ejecución (equivale a docker rm -f).

example:
  ```go
  // Eliminar contenedor detenido
  err := DockerRemoveContainer("old-app", false)

  // Forzar eliminación
  err = DockerRemoveContainer("stuck-app", true)
  ```

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerRemoveContainer elimina un contenedor Docker. Si force es true, fuerza la eliminación de contenedores en ejecución.
  func DockerRemoveContainer(nameOrID string, force bool) error {
  	args := []string{"rm"}
  	if force {
  		args = append(args, "-f")
  	}
  	args = append(args, nameOrID)

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("docker rm %s: %s", nameOrID, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"nameOrID","desc":"nombre o ID del contenedor Docker a eliminar"},{"name":"force","desc":"si true fuerza la eliminacion incluso si esta en ejecucion"}],"output":"nil si se elimino exitosamente, error en caso contrario"}


id: docker_remove_image_go_infra
name: docker_remove_image
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerRemoveImage(image string, force bool) error
tags: ["docker","image","remove","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
example:
  ```go
  err := DockerRemoveImage("nginx:latest", false)
  if err != nil {
  log.Fatal(err)
  }
  ```

tests: []
file_path: functions/infra/docker_remove_image.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 75bd2e6a88c726a985f284f32e73f3e7bacc2662aea1b682377eb7105d51935c
description:
  Elimina una imagen Docker local. Con force=true fuerza la eliminación incluso si hay contenedores que la usan.

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerRemoveImage elimina una imagen Docker local. Si force es true, fuerza la eliminación.
  func DockerRemoveImage(image string, force bool) error {
  	args := []string{"rmi"}
  	if force {
  		args = append(args, "-f")
  	}
  	args = append(args, image)

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("docker rmi %s: %s", image, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"image","desc":"referencia de imagen Docker (nombre:tag o ID)"},{"name":"force","desc":"si true fuerza eliminacion incluso si hay contenedores usando la imagen"}],"output":"nil si se elimino exitosamente, error en caso contrario"}


id: docker_remove_network_go_infra
name: docker_remove_network
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerRemoveNetwork(nameOrID string) error
description: Elimina una red Docker por nombre o ID.
tags: ["docker","network","remove","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
example:
  ```go
  err := DockerRemoveNetwork("my-app-net")
  if err != nil {
  log.Fatal(err)
  }
  ```

tests: []
file_path: functions/infra/docker_remove_network.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b8e55df608a90276c5b80315fe9559f0ef59c752916c19597d91b0abb805aa75
code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerRemoveNetwork elimina una red Docker por nombre o ID.
  func DockerRemoveNetwork(nameOrID string) error {
  	out, err := exec.Command("docker", "network", "rm", nameOrID).CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("docker network rm %s: %s", nameOrID, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"nameOrID","desc":"nombre o ID de la red Docker a eliminar"}],"output":"nil si se elimino exitosamente, error en caso contrario"}


id: docker_run_container_go_infra
name: docker_run_container
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerRunContainer(image string, opts DockerRunOpts) (string, error)
tags: ["docker","container","run","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
tests: []
file_path: functions/infra/docker_run_container.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 9763cbf4d185e50eea78e62562240e53d912531348b723dc27d91fce08990b3a
description:
  Ejecuta un contenedor Docker nuevo a partir de una imagen. Soporta puertos, env vars, volumes, network, detach y auto-remove. Devuelve el ID del contenedor.

example:
  ```go
  id, err := DockerRunContainer("nginx:latest", DockerRunOpts{
      Name:   "web-server",
      Ports:  []string{"8080:80"},
      Env:    map[string]string{"NGINX_HOST": "localhost"},
      Detach: true,
  })
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("Container ID:", id)
  ```

notes:
  `DockerRunOpts` es un struct de opciones definido en el mismo archivo. No se registra como tipo del registry porque es específico de esta función.

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerRunOpts opciones para ejecutar un contenedor Docker.
  type DockerRunOpts struct {
  	Name    string            // Nombre del contenedor (opcional)
  	Ports   []string          // Mapeo de puertos, ej: ["8080:80", "443:443"]
  	Env     map[string]string // Variables de entorno
  	Volumes []string          // Bind mounts, ej: ["/host/path:/container/path"]
  	Detach  bool              // Ejecutar en background
  	Remove  bool              // Eliminar al terminar (--rm)
  	Network string            // Red Docker (opcional)
  	CapAdd  []string          // Linux capabilities, ej: ["NET_ADMIN", "SYS_MODULE"]
  	Cmd     []string          // Comando a ejecutar en el contenedor (opcional)
  }

  // DockerRunContainer ejecuta un contenedor Docker nuevo con la imagen y opciones dadas.
  // Devuelve el ID del contenedor creado.
  func DockerRunContainer(image string, opts DockerRunOpts) (string, error) {
  	args := []string{"run"}

  	if opts.Detach {
  		args = append(args, "-d")
  	}
  	if opts.Remove {
  		args = append(args, "--rm")
  	}
  	if opts.Name != "" {
  		args = append(args, "--name", opts.Name)
  	}
  	if opts.Network != "" {
  		args = append(args, "--network", opts.Network)
  	}
  	for _, p := range opts.Ports {
  		args = append(args, "-p", p)
  	}
  	for k, v := range opts.Env {
  		args = append(args, "-e", k+"="+v)
  	}
  	for _, vol := range opts.Volumes {
  		args = append(args, "-v", vol)
  	}
  	for _, cap := range opts.CapAdd {
  		args = append(args, "--cap-add", cap)
  	}

  	args = append(args, image)
  	args = append(args, opts.Cmd...)

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker run %s: %s", image, strings.TrimSpace(string(out)))
  	}

  	return strings.TrimSpace(string(out)), nil
  }

params_schema:
  {"params":[{"name":"image","desc":"referencia de imagen Docker (nombre:tag)"},{"name":"opts","desc":"estructura DockerRunOpts con Name, Ports, Env, Volumes, Network, Detach, AutoRemove"}],"output":"ID del contenedor Docker creado y ejecutado"}


id: docker_start_container_go_infra
name: docker_start_container
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerStartContainer(nameOrID string) error
description: Inicia un contenedor Docker existente que está detenido. Recibe nombre o ID del contenedor.
tags: ["docker","container","start","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
example:
  ```go
  err := DockerStartContainer("my-app")
  if err != nil {
  log.Fatal(err)
  }
  ```

tests: []
file_path: functions/infra/docker_start_container.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7d6cec9302233040cd22b172c7e7f140bb52a733cc5ed536d5ab1658867edce0
code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerStartContainer inicia un contenedor existente por nombre o ID.
  func DockerStartContainer(nameOrID string) error {
  	out, err := exec.Command("docker", "start", nameOrID).CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("docker start %s: %s", nameOrID, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"nameOrID","desc":"nombre o ID del contenedor Docker a iniciar"}],"output":"nil si se inicio exitosamente, error en caso contrario"}


id: docker_stop_container_go_infra
name: docker_stop_container
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerStopContainer(nameOrID string, timeoutSecs int) error
tags: ["docker","container","stop","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strconv","strings"]
example:
  ```go
  err := DockerStopContainer("my-app", 10)
  if err != nil {
  log.Fatal(err)
  }
  ```

tests: []
file_path: functions/infra/docker_stop_container.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 2df076c63bc14346787358902331acd7decea0ffb17b44013d3fa3089706ec94
description:
  Detiene un contenedor Docker en ejecución. timeoutSecs controla el tiempo de gracia antes de SIGKILL (0 usa el default de Docker).

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strconv"
  	"strings"
  )

  // DockerStopContainer detiene un contenedor Docker. timeoutSecs indica el tiempo de gracia antes de SIGKILL.
  func DockerStopContainer(nameOrID string, timeoutSecs int) error {
  	args := []string{"stop"}
  	if timeoutSecs > 0 {
  		args = append(args, "-t", strconv.Itoa(timeoutSecs))
  	}
  	args = append(args, nameOrID)

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("docker stop %s: %s", nameOrID, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"nameOrID","desc":"nombre o ID del contenedor Docker a detener"},{"name":"timeoutSecs","desc":"segundos de gracia antes de SIGKILL (0 usa el default de Docker)"}],"output":"nil si se detuvo exitosamente, error en caso contrario"}


id: docker_volume_create_go_infra
name: docker_volume_create
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerVolumeCreate(name string) (string, error)
tags: ["docker","volume","create","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
tests: ["crea volume y retorna nombre","idempotente si el volume ya existe"]
test_file_path: functions/infra/docker_volume_create_test.go
file_path: functions/infra/docker_volume_create.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: cb90d18f5927318ac4f89f9bc5bcda4302059b73e56d4dea7d42b36dcbc75544
description:
  Crea un volume Docker con el nombre dado. Retorna el nombre del volume creado tal como lo confirma Docker.

example:
  ```go
  volumeName, err := DockerVolumeCreate("postgres_data")
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("Created volume:", volumeName)
  // Created volume: postgres_data
  ```

notes:
  Ejecuta `docker volume create name`. Docker imprime el nombre del volume creado en stdout. Idempotente si el volume ya existe con el mismo nombre.

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerVolumeCreate crea un volume Docker con el nombre dado.
  // Retorna el nombre del volume creado.
  func DockerVolumeCreate(name string) (string, error) {
  	out, err := exec.Command("docker", "volume", "create", name).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker volume create %s: %s", name, strings.TrimSpace(string(out)))
  	}
  	return strings.TrimSpace(string(out)), nil
  }

params_schema:
  {"params":[{"name":"name","desc":"nombre del volume Docker a crear"}],"output":"nombre del volume creado confirmado por Docker"}


id: docker_volume_list_go_infra
name: docker_volume_list
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerVolumeList() ([]map[string]string, error)
tags: ["docker","volume","list","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["encoding/json","fmt","os/exec","strings"]
tests: ["lista vacia retorna nil sin error","parsea campos Driver y Name correctamente"]
test_file_path: functions/infra/docker_volume_list_test.go
file_path: functions/infra/docker_volume_list.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 396f603c468dfe59892c0b11bd98908ea4c15e65932892c7e7724f0384b0f291
params_schema: {"output":"slice de maps con campos Driver, Name, Scope, Labels, Mountpoint de cada volume"}
description:
  Lista los volumes Docker disponibles localmente. Parsea la salida JSON de docker volume ls. Retorna slice de maps con campos Driver, Name, Scope, Labels, Mountpoint.

example:
  ```go
  volumes, err := DockerVolumeList()
  if err != nil {
      log.Fatal(err)
  }
  for _, v := range volumes {
      fmt.Printf("Volume: %s (driver: %s)\n", v["Name"], v["Driver"])
  }
  ```

notes:
  Ejecuta `docker volume ls --format {{json .}}` (un JSON por linea). Usa splitLines del paquete infra para iterar lineas. Retorna nil si no hay volumes. Los campos del map dependen de la version de Docker pero siempre incluyen Driver y Name.

code:
  package infra

  import (
  	"encoding/json"
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerVolumeList lista los volumes Docker disponibles localmente.
  // Retorna un slice de maps con campos Driver, Name, Scope, Labels, Mountpoint.
  func DockerVolumeList() ([]map[string]string, error) {
  	out, err := exec.Command("docker", "volume", "ls", "--format", "{{json .}}").Output()
  	if err != nil {
  		return nil, fmt.Errorf("docker volume ls: %w", err)
  	}

  	if len(strings.TrimSpace(string(out))) == 0 {
  		return nil, nil
  	}

  	var volumes []map[string]string
  	for _, line := range splitLines(out) {
  		if len(line) == 0 {
  			continue
  		}
  		var raw map[string]string
  		if err := json.Unmarshal(line, &raw); err != nil {
  			continue
  		}
  		volumes = append(volumes, raw)
  	}

  	return volumes, nil
  }


id: docker_volume_remove_go_infra
name: docker_volume_remove
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DockerVolumeRemove(name string, force bool) error
description: Elimina un volume Docker por nombre. Si force es true fuerza la eliminación aunque esté en uso.
tags: ["docker","volume","remove","delete","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os/exec","strings"]
tests: ["error si volume no existe","force flag incluye -f en el comando"]
test_file_path: functions/infra/docker_volume_remove_test.go
file_path: functions/infra/docker_volume_remove.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d13b4f34274ac72d844412b7e9605e506394c4e233e0484a140858b7581d9a62
example:
  ```go
  // Eliminar volume con fuerza
  err := DockerVolumeRemove("postgres_data", true)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("Volume eliminado")
  ```

notes:
  Ejecuta `docker volume rm [-f] name`. El flag -f solo esta disponible en versiones recientes de Docker. Sin force, falla si el volume esta siendo usado por un contenedor activo.

code:
  package infra

  import (
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // DockerVolumeRemove elimina un volume Docker por nombre.
  // Si force es true, fuerza la eliminación incluso si está en uso.
  func DockerVolumeRemove(name string, force bool) error {
  	args := []string{"volume", "rm"}
  	if force {
  		args = append(args, "-f")
  	}
  	args = append(args, name)

  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("docker volume rm %s: %s", name, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"name","desc":"nombre del volume Docker a eliminar"},{"name":"force","desc":"si true fuerza la eliminacion incluso si esta siendo usado"}],"output":"nil si se elimino exitosamente, error en caso contrario"}


id: duckdb_open_go_infra
name: duckdb_open
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func DuckDBOpen(path string) (*sql.DB, error)
description: Abre (o crea) una base de datos DuckDB. Path vacio o ':memory:' abre una base en memoria.
tags: ["database","duckdb","connection","sql","analytics"]
uses_functions: []
uses_types: ["db_config_go_infra"]
returns: []
error_type: error_go_core
imports: ["database/sql","github.com/marcboeker/go-duckdb"]
tests: []
file_path: functions/infra/duckdb_open.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: e72cfe0757201e5b25126c7f58cbf726b29a352615f0851e901a5b4051369c34
example:
  ```go
  // In-memory para analisis temporal
  db, err := DuckDBOpen("")
  if err != nil {
      log.Fatal(err)
  }
  defer DBClose(db)

  rows, err := DBQuery(db, "SELECT * FROM read_parquet('/data/sales.parquet')")
  ```

notes:
  Usa el driver `github.com/marcboeker/go-duckdb` (CGO). DuckDB es una base de datos OLAP embebida, ideal para analisis de datos. Path vacio equivale a `:memory:`. Hace ping al abrir para detectar errores temprano.

code:
  //go:build !noduckdb

  package infra

  import (
  	"database/sql"
  	"fmt"

  	_ "github.com/marcboeker/go-duckdb"
  )

  // DuckDBOpen opens (or creates) a DuckDB database file.
  // Pass an empty path or ":memory:" for an in-memory database.
  // Returns a ready-to-use *sql.DB or an error.
  func DuckDBOpen(path string) (*sql.DB, error) {
  	dsn := path
  	if dsn == "" {
  		dsn = ":memory:"
  	}
  	db, err := sql.Open("duckdb", dsn)
  	if err != nil {
  		return nil, fmt.Errorf("duckdb_open: open %q: %w", dsn, err)
  	}
  	if err := db.Ping(); err != nil {
  		db.Close()
  		return nil, fmt.Errorf("duckdb_open: ping %q: %w", dsn, err)
  	}
  	return db, nil
  }

params_schema:
  {"params":[{"name":"path","desc":"ruta del archivo DuckDB o vacio/:memory: para base en memoria"}],"output":"conexion sql.DB abierta a DuckDB"}


id: generate_dockerfile_go_infra
name: generate_dockerfile
kind: function
lang: go
domain: infra
version: 1.0.0
purity: pure
signature: func GenerateDockerfile(binaryName string, port int, envVars map[string]string) string
tags: ["docker","dockerfile","go","build","deploy","infra","pure"]
uses_functions: []
uses_types: []
returns: []
imports: ["fmt","sort","strings"]
test_file_path: functions/infra/generate_dockerfile_test.go
file_path: functions/infra/generate_dockerfile.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a5a84109011ff28a5dfd12c101744d0c6d1464c2bbe0eb272a4e1fb06b2277af
description:
  Genera el texto de un Dockerfile multi-stage para una app Go. Stage build con golang:1.23-alpine, stage final con alpine:latest. Incluye ENV vars del map con orden determinista. Funcion pura sin I/O.

example:
  ```go
  content := GenerateDockerfile("myapp", 8080, map[string]string{
      "DB_HOST": "localhost",
      "PORT":    "8080",
  })
  fmt.Println(content)
  // FROM golang:1.23-alpine AS builder
  // ...
  // EXPOSE 8080
  // ENTRYPOINT ["./myapp"]
  ```

tests:
  ["contiene stage builder con golang:1.23-alpine","contiene stage final con alpine:latest","incluye EXPOSE cuando port mayor a cero","no incluye EXPOSE cuando port es cero","env vars aparecen ordenadas alfabeticamente","binaryName aparece en ENTRYPOINT","env vars vacias no generan instrucciones ENV"]

notes:
  Funcion pura — no toca el sistema de archivos. Componer con WriteDockerfile para persistir el resultado. Las ENV vars se ordenan alfabeticamente para garantizar Dockerfiles deterministas (mismo input => mismo output exacto). El stage build usa CGO_ENABLED=0 para binarios estáticos compatibles con alpine. Si port <= 0, omite la instruccion EXPOSE.

code:
  package infra

  import (
  	"fmt"
  	"sort"
  	"strings"
  )

  // GenerateDockerfile genera el texto de un Dockerfile multi-stage para una app Go.
  // Stage build: golang:1.23-alpine — descarga dependencias y compila.
  // Stage final: alpine:latest — copia el binario, expone el puerto y lo ejecuta.
  // Las envVars se inyectan como instrucciones ENV en el stage final.
  // Funcion pura: no realiza I/O, solo genera texto.
  func GenerateDockerfile(binaryName string, port int, envVars map[string]string) string {
  	var sb strings.Builder

  	// Stage build
  	sb.WriteString("# Stage build\n")
  	sb.WriteString("FROM golang:1.23-alpine AS builder\n\n")
  	sb.WriteString("WORKDIR /app\n\n")
  	sb.WriteString("COPY go.mod go.sum ./\n")
  	sb.WriteString("RUN go mod download\n\n")
  	sb.WriteString("COPY . .\n")
  	sb.WriteString(fmt.Sprintf("RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags=\"-s -w\" -o %s .\n\n", binaryName))

  	// Stage final
  	sb.WriteString("# Stage final\n")
  	sb.WriteString("FROM alpine:latest\n\n")
  	sb.WriteString("RUN apk --no-cache add ca-certificates tzdata\n\n")
  	sb.WriteString("WORKDIR /app\n\n")
  	sb.WriteString(fmt.Sprintf("COPY --from=builder /app/%s .\n\n", binaryName))

  	// ENV vars (orden determinista)
  	if len(envVars) > 0 {
  		keys := make([]string, 0, len(envVars))
  		for k := range envVars {
  			keys = append(keys, k)
  		}
  		sort.Strings(keys)
  		for _, k := range keys {
  			sb.WriteString(fmt.Sprintf("ENV %s=%s\n", k, envVars[k]))
  		}
  		sb.WriteString("\n")
  	}

  	if port > 0 {
  		sb.WriteString(fmt.Sprintf("EXPOSE %d\n\n", port))
  	}

  	sb.WriteString(fmt.Sprintf("ENTRYPOINT [\"./%s\"]\n", binaryName))

  	return sb.String()
  }

params_schema:
  {"params":[{"name":"binaryName","desc":"nombre del binario Go a ejecutar en el contenedor"},{"name":"port","desc":"puerto a exponer en el Dockerfile (0 omite la instruccion EXPOSE)"},{"name":"envVars","desc":"mapa de variables de entorno a incluir en el Dockerfile"}],"output":"string con el contenido del Dockerfile multi-stage"}


id: go_build_binary_go_infra
name: go_build_binary
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func GoBuildBinary(projectDir, outputPath string, ldflags string, tags string) error
tags: ["go","build","binary","compile","infra","deploy"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os","os/exec","path/filepath","strings"]
test_file_path: functions/infra/go_build_binary_test.go
file_path: functions/infra/go_build_binary.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 98709b5938aadaace7fd56f9211521c76d9c8c58f067ee3b62fcba8dabc070b4
description:
  Compila un binario Go desde un directorio de proyecto. Si ldflags está vacío usa -s -w (strip debug). Si outputPath está vacío usa build/{dirname} dentro del projectDir. Ejecuta con CGO_ENABLED=0.

example:
  ```go
  // Compilar con opciones por defecto
  err := GoBuildBinary("/home/user/apps/myapp", "", "", "")
  // genera /home/user/apps/myapp/build/myapp

  // Compilar con output y tags explícitos
  err = GoBuildBinary("/home/user/apps/myapp", "/tmp/myapp-bin", "-s -w -X main.version=1.0", "fts5")
  if err != nil {
      log.Fatal(err)
  }
  ```

tests:
  ["compila proyecto valido sin error","outputPath vacio usa build/dirname por defecto","ldflags vacio usa -s -w por defecto","error si projectDir no existe"]

notes:
  Usa `CGO_ENABLED=0` para binarios estáticos compatibles con imágenes alpine. El flag `-trimpath` elimina rutas absolutas del binario para reproducibilidad. Los flags `-s -w` reducen el tamaño eliminando información de debug y la tabla de símbolos. Compatible con el flujo deploy_app que genera Dockerfile multi-stage.

code:
  package infra

  import (
  	"fmt"
  	"os"
  	"os/exec"
  	"path/filepath"
  	"strings"
  )

  // GoBuildBinary compila un binario Go desde projectDir hacia outputPath.
  // Si ldflags está vacío usa "-s -w" (strip debug info). Si outputPath está vacío
  // usa "build/{dirname}" dentro del projectDir. Requiere Go instalado en PATH.
  func GoBuildBinary(projectDir, outputPath string, ldflags string, tags string) error {
  	if ldflags == "" {
  		ldflags = "-s -w"
  	}
  	if outputPath == "" {
  		outputPath = filepath.Join(projectDir, "build", filepath.Base(projectDir))
  	}

  	// Crear directorio destino si no existe
  	if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
  		return fmt.Errorf("go_build_binary: crear directorio destino %s: %w", filepath.Dir(outputPath), err)
  	}

  	args := []string{"build", "-trimpath", "-ldflags=" + ldflags}
  	if tags != "" {
  		args = append(args, "-tags="+tags)
  	}
  	args = append(args, "-o", outputPath, ".")

  	cmd := exec.Command("go", args...)
  	cmd.Dir = projectDir
  	cmd.Env = append(os.Environ(), "CGO_ENABLED=0")

  	out, err := cmd.CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("go build en %s: %s", projectDir, strings.TrimSpace(string(out)))
  	}
  	return nil
  }

params_schema:
  {"params":[{"name":"projectDir","desc":"ruta del directorio Go (con main.go o go.mod)"},{"name":"outputPath","desc":"ruta donde guardar el binario o vacio para usar build/{dirname}"},{"name":"ldflags","desc":"flags de vinculacion Go o vacio para usar -s -w (strip)"},{"name":"tags","desc":"build tags Go (ej: fts5,cgo)"}],"output":"nil si la compilacion fue exitosa, error en caso contrario"}


id: health_check_http_go_infra
name: health_check_http
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func HealthCheckHTTP(url string, timeoutSecs, intervalMs int) error
tags: ["http","health","check","polling","wait","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","net/http","time"]
test_file_path: functions/infra/health_check_http_test.go
file_path: functions/infra/health_check_http.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 650dc42911137ce40ed6a29c7c75ba8185704e91ec278899ad53cb97687bcaee
description:
  Hace polling HTTP GET a un endpoint hasta recibir status 200 o hasta agotar el timeout. Útil para esperar que un servicio levante antes de continuar un pipeline.

example:
  ```go
  // Esperar hasta 60s a que Metabase levante, polling cada 2s
  err := HealthCheckHTTP("http://localhost:3000/api/health", 60, 2000)
  if err != nil {
      log.Fatal("Servicio no disponible:", err)
  }
  fmt.Println("Servicio listo")
  ```

tests:
  ["retorna nil cuando el servidor responde 200","retorna error si el timeout se agota","respeta el intervalo entre intentos"]

notes:
  Usa solo net/http de la stdlib, sin dependencias externas. El cliente HTTP tiene timeout de intervalMs + 500ms para no bloquear el loop. Retorna el ultimo error si el timeout expira. No sigue redirects especiales — cualquier respuesta 200 OK es exito.

code:
  package infra

  import (
  	"fmt"
  	"net/http"
  	"time"
  )

  // HealthCheckHTTP hace polling HTTP GET a url hasta recibir status 200
  // o hasta que pasen timeoutSecs segundos. Entre intentos espera intervalMs milisegundos.
  func HealthCheckHTTP(url string, timeoutSecs, intervalMs int) error {
  	deadline := time.Now().Add(time.Duration(timeoutSecs) * time.Second)
  	interval := time.Duration(intervalMs) * time.Millisecond

  	client := &http.Client{
  		Timeout: time.Duration(intervalMs)*time.Millisecond + 500*time.Millisecond,
  	}

  	var lastErr error
  	for time.Now().Before(deadline) {
  		resp, err := client.Get(url)
  		if err == nil {
  			resp.Body.Close()
  			if resp.StatusCode == http.StatusOK {
  				return nil
  			}
  			lastErr = fmt.Errorf("status %d", resp.StatusCode)
  		} else {
  			lastErr = err
  		}
  		time.Sleep(interval)
  	}

  	if lastErr != nil {
  		return fmt.Errorf("health check %s timeout after %ds: %w", url, timeoutSecs, lastErr)
  	}
  	return fmt.Errorf("health check %s timeout after %ds", url, timeoutSecs)
  }

params_schema:
  {"params":[{"name":"url","desc":"URL del endpoint HTTP a verificar (GET)"},{"name":"timeoutSecs","desc":"tiempo maximo en segundos antes de agotar los intentos"},{"name":"intervalMs","desc":"intervalo en milisegundos entre intentos de verificacion"}],"output":"nil si el endpoint responde 200, error si se agota el timeout"}


id: http_download_file_go_infra
name: http_download_file
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["http","download","file","streaming","atomic","network","stdlib","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","io","net/http","os","path/filepath","time"]
test_file_path: functions/infra/http_download_file_test.go
file_path: functions/infra/http_download_file.go
created_at: 2026-04-05T11:25:56Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: dd015de01e491493a67384343824c57a1fad3621fc4ac5323d402bf2002916f7
signature:
  func HttpDownloadFile(url, destPath string, headers map[string]string, timeout time.Duration) (int64, error)

description:
  Descarga url en destPath en streaming con io.Copy. Crea directorios con os.MkdirAll. Usa archivo temporal + rename para atomicidad (no deja archivo corrupto si falla). Retorna bytes escritos.

example:
  ```go
  n, err := HttpDownloadFile(
      "https://example.com/report.pdf",
      "/tmp/reports/report.pdf",
      nil,
      2*time.Minute,
  )
  if err != nil {
      return err
  }
  fmt.Printf("Downloaded %d bytes\n", n)
  ```

tests:
  ["httptest.Server sirve archivo binario","Directorio creado automaticamente","Archivo temporal + rename (no deja basura si falla)","Size retornado coincide"]

notes:
  Solo usa stdlib. El archivo temporal se crea en el mismo directorio que destPath para que el rename sea atomico (mismo filesystem). Si la descarga falla, el archivo temporal se elimina con os.Remove (el defer lo garantiza). Compatible con archivos de cualquier tamano ya que usa streaming con io.Copy.

code:
  package infra

  import (
  	"fmt"
  	"io"
  	"net/http"
  	"os"
  	"path/filepath"
  	"time"
  )

  // HttpDownloadFile descarga url en destPath en streaming con io.Copy.
  // Crea directorios intermedios con os.MkdirAll. Usa archivo temporal + rename
  // para garantizar atomicidad (no deja archivo corrupto si falla a mitad).
  // Retorna los bytes escritos.
  func HttpDownloadFile(url, destPath string, headers map[string]string, timeout time.Duration) (int64, error) {
  	client := &http.Client{Timeout: timeout}

  	req, err := http.NewRequest(http.MethodGet, url, nil)
  	if err != nil {
  		return 0, fmt.Errorf("http_download_file: build request: %w", err)
  	}
  	for k, v := range headers {
  		req.Header.Set(k, v)
  	}

  	resp, err := client.Do(req)
  	if err != nil {
  		return 0, fmt.Errorf("http_download_file: %w", err)
  	}
  	defer resp.Body.Close()

  	if resp.StatusCode >= 400 {
  		shortURL := url
  		if len(shortURL) > 100 {
  			shortURL = shortURL[:100]
  		}
  		return 0, fmt.Errorf("http_download_file: HTTP %d at %q", resp.StatusCode, shortURL)
  	}

  	dir := filepath.Dir(destPath)
  	if err := os.MkdirAll(dir, 0o755); err != nil {
  		return 0, fmt.Errorf("http_download_file: create dirs: %w", err)
  	}

  	// Archivo temporal en el mismo directorio para que rename sea atomico
  	tmp, err := os.CreateTemp(dir, ".download-*")
  	if err != nil {
  		return 0, fmt.Errorf("http_download_file: create temp file: %w", err)
  	}
  	tmpPath := tmp.Name()
  	defer func() {
  		tmp.Close()
  		os.Remove(tmpPath) // no-op si rename tuvo exito
  	}()

  	n, err := io.Copy(tmp, resp.Body)
  	if err != nil {
  		return 0, fmt.Errorf("http_download_file: write: %w", err)
  	}

  	if err := tmp.Close(); err != nil {
  		return 0, fmt.Errorf("http_download_file: close temp: %w", err)
  	}

  	if err := os.Rename(tmpPath, destPath); err != nil {
  		return 0, fmt.Errorf("http_download_file: rename: %w", err)
  	}

  	return n, nil
  }

params_schema:
  {"params":[{"name":"url","desc":"URL del archivo a descargar"},{"name":"destPath","desc":"ruta de destino del archivo descargado"},{"name":"headers","desc":"mapa de headers custom a incluir en la solicitud"},{"name":"timeout","desc":"duracion maxima para completar la descarga"}],"output":"numero de bytes descargados exitosamente"}


id: http_get_json_go_infra
name: http_get_json
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["http","json","get","client","network","stdlib","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["encoding/json","fmt","io","net/http","time"]
tests: ["httptest.Server con respuesta JSON","Status 404 → error","Timeout → error","Headers custom"]
test_file_path: functions/infra/http_get_json_test.go
file_path: functions/infra/http_get_json.go
created_at: 2026-04-05T11:25:56Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: cf0392ffb51629fb2e1ae74d742314641fd5348ce9b2d598c1b91492dc82a545
signature:
  func HttpGetJSON(url string, headers map[string]string, timeout time.Duration) (map[string]any, error)

description:
  GET request que espera JSON. Agrega Accept: application/json automaticamente. Retorna error con status code si >= 400. Siempre cierra body con defer.

example:
  ```go
  result, err := HttpGetJSON(
      "https://api.example.com/users",
      map[string]string{"X-Api-Key": "secret"},
      10*time.Second,
  )
  if err != nil {
      return nil, err
  }
  fmt.Println(result["total"])
  ```

notes:
  Solo usa stdlib (net/http, encoding/json). El timeout se configura en el http.Client. El error incluye los primeros 200 bytes del body para facilitar debugging. Los headers custom se fusionan con Accept: application/json (custom tiene precedencia).

code:
  package infra

  import (
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"time"
  )

  // HttpGetJSON realiza un GET request a url y parsea la respuesta como JSON.
  // Agrega Accept: application/json automaticamente. Retorna error si status >= 400
  // incluyendo el status code y los primeros 200 bytes del body.
  func HttpGetJSON(url string, headers map[string]string, timeout time.Duration) (map[string]any, error) {
  	client := &http.Client{Timeout: timeout}

  	req, err := http.NewRequest(http.MethodGet, url, nil)
  	if err != nil {
  		return nil, fmt.Errorf("http_get_json: build request: %w", err)
  	}

  	req.Header.Set("Accept", "application/json")
  	for k, v := range headers {
  		req.Header.Set(k, v)
  	}

  	resp, err := client.Do(req)
  	if err != nil {
  		return nil, fmt.Errorf("http_get_json: %w", err)
  	}
  	defer resp.Body.Close()

  	body, err := io.ReadAll(resp.Body)
  	if err != nil {
  		return nil, fmt.Errorf("http_get_json: read body: %w", err)
  	}

  	if resp.StatusCode >= 400 {
  		preview := body
  		if len(preview) > 200 {
  			preview = preview[:200]
  		}
  		shortURL := url
  		if len(shortURL) > 100 {
  			shortURL = shortURL[:100]
  		}
  		return nil, fmt.Errorf("http_get_json: HTTP %d at %q — %s", resp.StatusCode, shortURL, preview)
  	}

  	var result map[string]any
  	if err := json.Unmarshal(body, &result); err != nil {
  		return nil, fmt.Errorf("http_get_json: parse JSON: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"url","desc":"URL del endpoint HTTP a consultar"},{"name":"headers","desc":"mapa de headers custom a incluir en la solicitud"},{"name":"timeout","desc":"duracion maxima para completar la solicitud"}],"output":"mapa con la estructura JSON decodificada del response"}


id: http_post_json_go_infra
name: http_post_json
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["http","json","post","client","network","stdlib","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["bytes","encoding/json","fmt","io","net/http","time"]
tests: ["httptest.Server recibe body correcto","Status 201 → exito","Status 500 → error con body parcial"]
test_file_path: functions/infra/http_post_json_test.go
file_path: functions/infra/http_post_json.go
created_at: 2026-04-05T11:25:56Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5e9198ac0d3036b2edf85c00b4ef56d0d9c2049bfc97937e17601d403a6425c4
signature:
  func HttpPostJSON(url string, body any, headers map[string]string, timeout time.Duration) (map[string]any, error)

description:
  POST request con body JSON serializado con json.Marshal. Agrega Content-Type: application/json y Accept: application/json. Retorna error con status code si >= 400.

example:
  ```go
  result, err := HttpPostJSON(
      "https://api.example.com/users",
      map[string]any{"name": "Alice", "role": "admin"},
      map[string]string{"X-Api-Key": "secret"},
      10*time.Second,
  )
  if err != nil {
      return nil, err
  }
  fmt.Println(result["id"])
  ```

notes:
  Solo usa stdlib. El body acepta `any` y se serializa con json.Marshal. Headers custom se fusionan con Content-Type y Accept por defecto. El error incluye los primeros 200 bytes del body de respuesta.

code:
  package infra

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"time"
  )

  // HttpPostJSON realiza un POST request con body JSON y parsea la respuesta como JSON.
  // Agrega Content-Type: application/json y Accept: application/json automaticamente.
  // Retorna error si status >= 400 incluyendo status code y los primeros 200 bytes del body.
  func HttpPostJSON(url string, body any, headers map[string]string, timeout time.Duration) (map[string]any, error) {
  	data, err := json.Marshal(body)
  	if err != nil {
  		return nil, fmt.Errorf("http_post_json: marshal body: %w", err)
  	}

  	client := &http.Client{Timeout: timeout}

  	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
  	if err != nil {
  		return nil, fmt.Errorf("http_post_json: build request: %w", err)
  	}

  	req.Header.Set("Content-Type", "application/json")
  	req.Header.Set("Accept", "application/json")
  	for k, v := range headers {
  		req.Header.Set(k, v)
  	}

  	resp, err := client.Do(req)
  	if err != nil {
  		return nil, fmt.Errorf("http_post_json: %w", err)
  	}
  	defer resp.Body.Close()

  	respBody, err := io.ReadAll(resp.Body)
  	if err != nil {
  		return nil, fmt.Errorf("http_post_json: read body: %w", err)
  	}

  	if resp.StatusCode >= 400 {
  		preview := respBody
  		if len(preview) > 200 {
  			preview = preview[:200]
  		}
  		shortURL := url
  		if len(shortURL) > 100 {
  			shortURL = shortURL[:100]
  		}
  		return nil, fmt.Errorf("http_post_json: HTTP %d at %q — %s", resp.StatusCode, shortURL, preview)
  	}

  	var result map[string]any
  	if err := json.Unmarshal(respBody, &result); err != nil {
  		return nil, fmt.Errorf("http_post_json: parse JSON: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"url","desc":"URL del endpoint HTTP"},{"name":"body","desc":"datos a serializar como JSON en el cuerpo de la solicitud"},{"name":"headers","desc":"mapa de headers custom a fusionar con los headers por defecto"},{"name":"timeout","desc":"duracion maxima para completar la solicitud"}],"output":"mapa con la estructura JSON decodificada del response"}


id: metabase_auth_go_infra
name: metabase_auth
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error)
tags: ["metabase","auth","session","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: ["MetabaseClient_go_infra"]
error_type: error_go_core
imports: ["bytes","encoding/json","fmt","io","net/http"]
tests: []
file_path: functions/infra/metabase_auth.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b40c6ef04255880f42b4ba5509e3557898a991b1f2fe0bda543226baa886e73b
description:
  Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias (configurable con MAX_SESSION_AGE en Metabase). Endpoint: POST /api/session.

example:
  ```go
  // Autenticar con credenciales
  client, err := MetabaseAuth("http://localhost:3000", "admin@example.com", "password123")
  if err != nil {
      log.Fatal(err)
  }
  // client.Token contiene el session token

  // Alternativa: usar API key directamente
  client := MetabaseNewClient("http://localhost:3000", "mb_api_key_xxxxx")
  ```

notes:
  Dos formas de obtener un MetabaseClient:
  - `MetabaseAuth`: login con email/password, obtiene session token via POST /api/session. Token expira en 14 dias por defecto.
  - `MetabaseNewClient`: usa una API key creada en el admin UI. No expira. Recomendado para automatizacion.

  El token se envia como header `X-Metabase-Session` en todas las llamadas subsiguientes.

  ### Para un LLM que use estas funciones

  1. Primero obtener un client con `MetabaseAuth()` o `MetabaseNewClient()`
  2. Pasar el client a todas las funciones CRUD (usuarios, cards, dashboards)
  3. Si recibes error 401, el token expiro — re-autenticar
  4. Rate limiting: Metabase limita intentos de login fallidos

code:
  package infra

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  )

  // MetabaseAuth autentica con email y password contra una instancia Metabase.
  // Retorna un MetabaseClient con el session token listo para usar.
  // baseURL es la URL base sin trailing slash (ej: "http://localhost:3000").
  func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error) {
  	payload, _ := json.Marshal(map[string]string{
  		"username": email,
  		"password": password,
  	})

  	resp, err := http.Post(baseURL+"/api/session", "application/json", bytes.NewReader(payload))
  	if err != nil {
  		return MetabaseClient{}, fmt.Errorf("metabase auth: %w", err)
  	}
  	defer resp.Body.Close()

  	body, err := io.ReadAll(resp.Body)
  	if err != nil {
  		return MetabaseClient{}, fmt.Errorf("read auth response: %w", err)
  	}

  	if resp.StatusCode != 200 {
  		return MetabaseClient{}, fmt.Errorf("metabase auth: status %d: %s", resp.StatusCode, string(body))
  	}

  	var result struct {
  		ID string `json:"id"`
  	}
  	if err := json.Unmarshal(body, &result); err != nil {
  		return MetabaseClient{}, fmt.Errorf("parse auth response: %w", err)
  	}

  	return MetabaseClient{BaseURL: baseURL, Token: result.ID}, nil
  }

  // MetabaseNewClient crea un MetabaseClient usando una API key en lugar de session token.
  // Las API keys se crean en Settings > Authentication > API Keys del admin de Metabase.
  func MetabaseNewClient(baseURL, apiKey string) MetabaseClient {
  	return MetabaseClient{BaseURL: baseURL, Token: apiKey}
  }

params_schema:
  {"params":[{"name":"baseURL","desc":"URL base de Metabase (ej: http://localhost:3000)"},{"name":"email","desc":"email de la cuenta para autenticarse"},{"name":"password","desc":"contraseña de la cuenta"}],"output":"cliente MetabaseClient con session token valido por 14 dias"}


id: metabase_create_card_go_infra
name: metabase_create_card
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
description: Crea una nueva card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card.
tags: ["metabase","card","question","create","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_create_card.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 89423d94986dae277ea7ba6a867840b734c837329272c2bd51704d93a16aae30
signature:
  func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error)

example:
  ```go
  // Crear pregunta con SQL nativo
  card, err := MetabaseCreateCard(client, "Revenue by Month", map[string]any{
      "database": 1,
      "type":     "native",
      "native": map[string]any{
          "query": "SELECT date_trunc('month', created_at) as month, SUM(total) as revenue FROM orders GROUP BY 1 ORDER BY 1",
      },
  }, "line", 5, "Monthly revenue trend")

  // Crear pregunta con MBQL (structured query)
  card, err := MetabaseCreateCard(client, "Order Count", map[string]any{
      "database": 1,
      "type":     "query",
      "query": map[string]any{
          "source-table": 4,
          "aggregation":  []any{[]any{"count"}},
      },
  }, "scalar", 0, "Total number of orders")
  ```

notes:
  ### Parametros para un LLM

  | Parametro | Tipo | Requerido | Descripcion |
  |-----------|------|-----------|-------------|
  | client | MetabaseClient | si | Cliente autenticado |
  | name | string | si | Nombre de la pregunta |
  | datasetQuery | map[string]any | si | Query. Ver estructura abajo |
  | display | string | si | Tipo de visualizacion |
  | collectionID | int | no | ID de coleccion. 0 = root collection |
  | description | string | no | Descripcion. Vacio = sin descripcion |

  ### Estructura de datasetQuery

  **SQL nativo:**
  ```json
  {
    "database": <database_id>,
    "type": "native",
    "native": {"query": "SELECT ..."}
  }
  ```

  **MBQL (structured):**
  ```json
  {
    "database": <database_id>,
    "type": "query",
    "query": {
      "source-table": <table_id>,
      "aggregation": [["count"]],
      "breakout": [["field", <field_id>, {"temporal-unit": "month"}]],
      "filter": ["=", ["field", <field_id>, null], "value"]
    }
  }
  ```

  ### Valores de display

  table, bar, line, pie, scalar, area, row, combo, funnel, map, scatter, waterfall, progress, gauge

code:
  package infra

  import "fmt"

  // MetabaseCreateCard crea una nueva card/pregunta en Metabase.
  // name: nombre de la pregunta (obligatorio).
  // datasetQuery: query de la card (obligatorio). Estructura:
  //
  //	SQL nativo: {"database": 1, "type": "native", "native": {"query": "SELECT ..."}}
  //	MBQL:       {"database": 1, "type": "query", "query": {"source-table": 4, ...}}
  //
  // display: tipo de visualizacion ("table", "bar", "line", "pie", "scalar", etc.).
  // collectionID: ID de la coleccion/carpeta (0 = root).
  // description: descripcion opcional (vacio = sin descripcion).
  func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error) {
  	body := map[string]any{
  		"name":                   name,
  		"dataset_query":          datasetQuery,
  		"display":                display,
  		"visualization_settings": map[string]any{},
  	}
  	if collectionID > 0 {
  		body["collection_id"] = collectionID
  	}
  	if description != "" {
  		body["description"] = description
  	}

  	result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/card", body)
  	if err != nil {
  		return nil, fmt.Errorf("metabase create card: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"name","desc":"nombre de la pregunta/card"},{"name":"datasetQuery","desc":"estructura de query SQL nativa o MBQL (structured query)"},{"name":"display","desc":"tipo de visualizacion (table, bar, line, pie, scalar, etc.)"},{"name":"collectionID","desc":"ID de la coleccion destino (0 = root collection)"},{"name":"description","desc":"descripcion de la pregunta o vacio"}],"output":"mapa con los detalles de la card creada incluyendo su ID"}


id: metabase_create_dashboard_go_infra
name: metabase_create_dashboard
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","dashboard","create","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_create_dashboard.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 060ba8d26bbdd49212d555ad3a4526910623d42bc07108ef55462cf11742e916
signature:
  func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error)

description:
  Crea un nuevo dashboard vacio en Metabase. Para agregar cards usar MetabaseUpdateDashboard con el campo dashcards. Endpoint: POST /api/dashboard.

example:
  ```go
  // Crear dashboard vacio
  dashboard, err := MetabaseCreateDashboard(client, "Sales Overview", "KPIs de ventas", 5)
  if err != nil {
      log.Fatal(err)
  }
  dashboardID := int(dashboard["id"].(float64))

  // Luego agregar cards con MetabaseUpdateDashboard
  MetabaseUpdateDashboard(client, dashboardID, map[string]any{
      "dashcards": []map[string]any{
          {"id": -1, "card_id": 42, "size_x": 6, "size_y": 4, "col": 0, "row": 0},
      },
  })
  ```

notes:
  ### Parametros para un LLM

  | Parametro | Tipo | Requerido | Descripcion |
  |-----------|------|-----------|-------------|
  | client | MetabaseClient | si | Cliente autenticado |
  | name | string | si | Nombre del dashboard |
  | description | string | no | Descripcion. Vacio = sin descripcion |
  | collectionID | int | no | Coleccion destino. 0 = root |

  El dashboard se crea vacio. Para agregar cards, usar MetabaseUpdateDashboard con el array dashcards.
  Retorna el objeto dashboard creado.

code:
  package infra

  import "fmt"

  // MetabaseCreateDashboard crea un nuevo dashboard en Metabase.
  // name: nombre del dashboard (obligatorio).
  // description: descripcion opcional (vacio = sin descripcion).
  // collectionID: ID de la coleccion/carpeta (0 = root).
  func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error) {
  	body := map[string]any{
  		"name": name,
  	}
  	if description != "" {
  		body["description"] = description
  	}
  	if collectionID > 0 {
  		body["collection_id"] = collectionID
  	}

  	result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dashboard", body)
  	if err != nil {
  		return nil, fmt.Errorf("metabase create dashboard: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"name","desc":"nombre del dashboard"},{"name":"description","desc":"descripcion del dashboard o vacio"},{"name":"collectionID","desc":"ID de la coleccion destino (0 = root collection)"}],"output":"mapa con los detalles del dashboard creado incluyendo su ID"}


id: metabase_create_user_go_infra
name: metabase_create_user
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","user","create","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_create_user.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: e4ee437830f0521ebb49d3d5b8dd03bbc4d681a93c15e983e4d35a8bc9ed7849
signature:
  func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error)

description:
  Crea un nuevo usuario en Metabase. Si no se provee password, Metabase envia email de invitacion. Requiere permisos de superusuario. Endpoint: POST /api/user.

example:
  ```go
  // Crear usuario con password
  user, err := MetabaseCreateUser(client, "John", "Doe", "john@example.com", "securePass123", nil)

  // Crear usuario sin password (envia invitacion por email)
  user, err := MetabaseCreateUser(client, "Jane", "Smith", "jane@example.com", "", []int{1, 3})
  ```

notes:
  ### Parametros para un LLM

  | Parametro | Tipo | Requerido | Descripcion |
  |-----------|------|-----------|-------------|
  | client | MetabaseClient | si | Cliente autenticado con permisos admin |
  | firstName | string | si | Nombre del usuario |
  | lastName | string | si | Apellido del usuario |
  | email | string | si | Email unico del usuario |
  | password | string | no | Password. Vacio = Metabase envia invitacion |
  | groupIDs | []int | no | IDs de grupos. nil = solo grupo default |

  El email debe ser unico. Si ya existe, retorna error 400.
  Retorna el objeto usuario creado como map (mismos campos que MetabaseGetUser).

code:
  package infra

  import "fmt"

  // MetabaseCreateUser crea un nuevo usuario en Metabase.
  // firstName, lastName y email son obligatorios.
  // password es opcional: si esta vacio, Metabase envia email de invitacion.
  // groupIDs es opcional: IDs de grupos a asignar (nil = solo grupo default).
  func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error) {
  	body := map[string]any{
  		"first_name": firstName,
  		"last_name":  lastName,
  		"email":      email,
  	}
  	if password != "" {
  		body["password"] = password
  	}
  	if len(groupIDs) > 0 {
  		body["group_ids"] = groupIDs
  	}

  	result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/user", body)
  	if err != nil {
  		return nil, fmt.Errorf("metabase create user: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado con permisos admin"},{"name":"firstName","desc":"nombre del usuario"},{"name":"lastName","desc":"apellido del usuario"},{"name":"email","desc":"email unico del usuario"},{"name":"password","desc":"contraseña o vacio para enviar invitacion por email"},{"name":"groupIDs","desc":"slice de IDs de grupos a los que agregar el usuario (nil = grupo default)"}],"output":"mapa con los detalles del usuario creado incluyendo su ID"}


id: metabase_deactivate_user_go_infra
name: metabase_deactivate_user
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseDeactivateUser(client MetabaseClient, userID int) error
tags: ["metabase","user","delete","deactivate","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_deactivate_user.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 578d28a8c502794f881145383f6bc346aad1551bdac22386673d849869cb8eb8
description:
  Desactiva (soft-delete) un usuario en Metabase. El usuario no se elimina permanentemente, solo se marca como inactivo. Para reactivar, usar PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id.

example:
  ```go
  err := MetabaseDeactivateUser(client, 5)
  if err != nil {
      log.Fatal(err)
  }
  // Usuario 5 ahora esta inactivo
  // Para ver desactivados: MetabaseListUsers(client, "deactivated", "", 0, 0)
  ```

notes:
  Es un soft-delete: el usuario se desactiva pero no se borra. Se puede reactivar con PUT /api/user/:id/reactivate.

  Para listar usuarios desactivados, usar `MetabaseListUsers` con status "deactivated".

  Requiere permisos de superusuario. Error 403 si no eres admin.

code:
  package infra

  import "fmt"

  // MetabaseDeactivateUser desactiva (soft-delete) un usuario en Metabase.
  // El usuario no se elimina permanentemente, solo se marca como inactivo.
  // Requiere permisos de superusuario.
  func MetabaseDeactivateUser(client MetabaseClient, userID int) error {
  	path := fmt.Sprintf("/api/user/%d", userID)

  	_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return fmt.Errorf("metabase deactivate user %d: %w", userID, err)
  	}

  	return nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado con permisos admin"},{"name":"userID","desc":"ID del usuario a desactivar"}],"output":"nil si la desactivacion fue exitosa, error en caso contrario"}


id: metabase_delete_card_go_infra
name: metabase_delete_card
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseDeleteCard(client MetabaseClient, cardID int) error
tags: ["metabase","card","question","delete","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_delete_card.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: dff069f36609ad63008f1d96656a722776b15018a150bc70c4c36946486f8f8d
description:
  Elimina permanentemente una card/pregunta de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateCard con archived:true. Endpoint: DELETE /api/card/:id.

example:
  ```go
  // Eliminar permanentemente
  err := MetabaseDeleteCard(client, 42)

  // Preferir soft-delete cuando sea posible:
  // MetabaseUpdateCard(client, 42, map[string]any{"archived": true})
  ```

notes:
  **ATENCION**: Esta operacion es irreversible. La card se elimina permanentemente.

  Para un borrado seguro, preferir archivar con `MetabaseUpdateCard(client, cardID, map[string]any{"archived": true})` que permite recuperar la card despues.

code:
  package infra

  import "fmt"

  // MetabaseDeleteCard elimina permanentemente una card/pregunta de Metabase.
  // Para soft-delete, usar MetabaseUpdateCard con archived: true.
  func MetabaseDeleteCard(client MetabaseClient, cardID int) error {
  	path := fmt.Sprintf("/api/card/%d", cardID)

  	_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return fmt.Errorf("metabase delete card %d: %w", cardID, err)
  	}

  	return nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"cardID","desc":"ID de la card/pregunta a eliminar"}],"output":"nil si la eliminacion fue exitosa, error en caso contrario"}


id: metabase_delete_dashboard_go_infra
name: metabase_delete_dashboard
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error
tags: ["metabase","dashboard","delete","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_delete_dashboard.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3324082719bcd85418c7564c9c1caca517b92094dc3b94cbcf106b23aff7783b
description:
  Elimina permanentemente un dashboard de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateDashboard con archived:true. Endpoint: DELETE /api/dashboard/:id.

example:
  ```go
  // Eliminar permanentemente
  err := MetabaseDeleteDashboard(client, 1)

  // Preferir soft-delete:
  // MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
  ```

notes:
  **ATENCION**: Esta operacion es irreversible. El dashboard y todas sus dashcards se eliminan permanentemente.

  Para un borrado seguro, preferir archivar con `MetabaseUpdateDashboard(client, dashboardID, map[string]any{"archived": true})`.

code:
  package infra

  import "fmt"

  // MetabaseDeleteDashboard elimina permanentemente un dashboard de Metabase.
  // Para soft-delete, usar MetabaseUpdateDashboard con archived: true.
  func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error {
  	path := fmt.Sprintf("/api/dashboard/%d", dashboardID)

  	_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return fmt.Errorf("metabase delete dashboard %d: %w", dashboardID, err)
  	}

  	return nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"dashboardID","desc":"ID del dashboard a eliminar"}],"output":"nil si la eliminacion fue exitosa, error en caso contrario"}


id: metabase_execute_card_go_infra
name: metabase_execute_card
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","card","question","execute","query","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_execute_card.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5f3602a369b6a99be696cf9a0f0da08407afba1176800ac0c4f6e63dc30ae298
signature:
  func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error)

description:
  Ejecuta la query de una card/pregunta guardada en Metabase y retorna los resultados. Soporta parametros para queries parametrizadas. Endpoint: POST /api/card/:id/query.

example:
  ```go
  // Ejecutar sin parametros
  result, err := MetabaseExecuteCard(client, 42, nil)
  if err != nil {
      log.Fatal(err)
  }
  data := result["data"].(map[string]any)
  rows := data["rows"].([]any)
  fmt.Printf("Filas: %d\n", len(rows))

  // Ejecutar con parametros
  result, err := MetabaseExecuteCard(client, 42, []map[string]any{
      {
          "type":   "category",
          "target": []any{"variable", []any{"template-tag", "status"}},
          "value":  "active",
      },
  })
  ```

notes:
  ### Estructura de la respuesta

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | status | string | "completed" o "failed" |
  | row_count | float64 | Numero de filas |
  | running_time | float64 | Tiempo de ejecucion en ms |
  | data.columns | []string | Nombres de columnas |
  | data.rows | [][]any | Filas de datos |
  | data.cols | []map | Metadata de columnas (name, base_type, display_name) |
  | data.native_form.query | string | SQL ejecutado |

  ### Parametros para queries parametrizadas

  ```go
  []map[string]any{
      {
          "type":   "category",                                      // tipo del parametro
          "target": []any{"variable", []any{"template-tag", "tag"}}, // referencia al template-tag
          "value":  "valor",                                         // valor a inyectar
      },
  }
  ```

  Limite por defecto: 2000 filas. Para queries ad-hoc sin card, usar MetabaseExecuteQuery.

code:
  package infra

  import "fmt"

  // MetabaseExecuteCard ejecuta la query de una card/pregunta guardada.
  // parameters: parametros de la query (nil si no tiene parametros).
  // Retorna los resultados con columnas y filas.
  func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error) {
  	path := fmt.Sprintf("/api/card/%d/query", cardID)

  	var body map[string]any
  	if len(parameters) > 0 {
  		body = map[string]any{"parameters": parameters}
  	}

  	result, err := metabaseRequest("POST", client.BaseURL, client.Token, path, body)
  	if err != nil {
  		return nil, fmt.Errorf("metabase execute card %d: %w", cardID, err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"cardID","desc":"ID de la card/pregunta guardada a ejecutar"},{"name":"parameters","desc":"slice de maps con parametros para queries parametrizadas (nil si no hay parametros)"}],"output":"mapa con status, row_count, running_time, data.columns, data.rows, data.native_form.query"}


id: metabase_execute_query_go_infra
name: metabase_execute_query
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","query","execute","sql","dataset","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_execute_query.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 714e0f7cf8fa5ec70503e6afc687fea4f4671b8cd307a76a4f8c2226dc3d6449
signature:
  func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error)

description:
  Ejecuta una query SQL ad-hoc contra una database de Metabase sin guardarla como card. Util para consultas rapidas y exploracion. Endpoint: POST /api/dataset.

example:
  ```go
  // Query simple
  result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM users LIMIT 10", 0)
  if err != nil {
      log.Fatal(err)
  }
  data := result["data"].(map[string]any)
  rows := data["rows"].([]any)

  // Query con limite custom
  result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM orders", 5000)
  ```

notes:
  ### Parametros para un LLM

  | Parametro | Tipo | Requerido | Descripcion |
  |-----------|------|-----------|-------------|
  | client | MetabaseClient | si | Cliente autenticado |
  | databaseID | int | si | ID de la database en Metabase (obtener con GET /api/database) |
  | sql | string | si | Query SQL a ejecutar |
  | maxResults | int | no | Limite de filas. 0 = default 2000 |

  ### Diferencia con MetabaseExecuteCard

  - `MetabaseExecuteQuery`: query ad-hoc, no se guarda. Usa POST /api/dataset.
  - `MetabaseExecuteCard`: ejecuta una card ya guardada. Usa POST /api/card/:id/query.

  Usar esta funcion para exploracion rapida. Si la query se va a reutilizar, crear una card con MetabaseCreateCard.

  ### Estructura de la respuesta

  Misma estructura que MetabaseExecuteCard:
  - `data.columns`: nombres de columnas
  - `data.rows`: filas de datos
  - `row_count`: numero de filas
  - `running_time`: tiempo en ms
  - `status`: "completed" o "failed"

code:
  package infra

  import "fmt"

  // MetabaseExecuteQuery ejecuta una query ad-hoc (sin guardar como card) en Metabase.
  // databaseID: ID de la base de datos en Metabase.
  // sql: query SQL a ejecutar.
  // maxResults: limite de filas (0 = default 2000 de Metabase).
  func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error) {
  	body := map[string]any{
  		"database": databaseID,
  		"type":     "native",
  		"native":   map[string]any{"query": sql},
  	}
  	if maxResults > 0 {
  		body["constraints"] = map[string]any{
  			"max-results":          maxResults,
  			"max-results-bare-rows": maxResults,
  		}
  	}

  	result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dataset", body)
  	if err != nil {
  		return nil, fmt.Errorf("metabase execute query: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"databaseID","desc":"ID de la database en Metabase"},{"name":"sql","desc":"query SQL a ejecutar"},{"name":"maxResults","desc":"limite de filas (0 = default 2000)"}],"output":"mapa con status, row_count, running_time, data.columns, data.rows"}


id: metabase_get_card_go_infra
name: metabase_get_card
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error)
tags: ["metabase","card","question","get","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_get_card.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 9398fb8fcecc57c982dee969c92b9feea719adc7a1c4e337b849aeb75f43c7c7
description:
  Obtiene los detalles completos de una card/pregunta de Metabase por su ID. Incluye la query, visualizacion y metadata. Endpoint: GET /api/card/:id.

example:
  ```go
  card, err := MetabaseGetCard(client, 42)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(card["name"], card["display"])
  ```

notes:
  Retorna el objeto card completo. Error 404 si no existe.

  ### Campos principales

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | id | float64 | ID de la card |
  | name | string | Nombre |
  | description | string | Descripcion |
  | display | string | Tipo visualizacion |
  | dataset_query | map | Query (native.query para SQL, query para MBQL) |
  | visualization_settings | map | Config de visualizacion |
  | collection_id | float64 | Coleccion contenedora |
  | database_id | float64 | Database asociada |
  | archived | bool | Archivada |
  | creator | map | Objeto del usuario creador |
  | created_at | string | Fecha creacion |
  | updated_at | string | Fecha actualizacion |

code:
  package infra

  import "fmt"

  // MetabaseGetCard obtiene una card/pregunta de Metabase por su ID.
  func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error) {
  	path := fmt.Sprintf("/api/card/%d", cardID)

  	result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return nil, fmt.Errorf("metabase get card %d: %w", cardID, err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"cardID","desc":"ID de la card/pregunta"}],"output":"mapa con detalles completos de la card (nombre, query, visualizacion, etc.)"}


id: metabase_get_dashboard_go_infra
name: metabase_get_dashboard
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error)
tags: ["metabase","dashboard","get","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_get_dashboard.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 8e43345b2af0f1a4300ad479e4d5fac28187f6eb29d3397e01cab7166884ab04
description:
  Obtiene un dashboard completo de Metabase incluyendo todas sus dashcards (cards posicionadas en el dashboard), tabs y parametros. Endpoint: GET /api/dashboard/:id.

example:
  ```go
  dashboard, err := MetabaseGetDashboard(client, 1)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(dashboard["name"])

  // Acceder a las cards del dashboard
  dashcards := dashboard["dashcards"].([]any)
  for _, dc := range dashcards {
      card := dc.(map[string]any)
      fmt.Printf("Card ID: %v, Position: (%v, %v)\n",
          card["card_id"], card["col"], card["row"])
  }
  ```

notes:
  ### Campos principales

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | id | float64 | ID del dashboard |
  | name | string | Nombre |
  | description | string | Descripcion |
  | dashcards | []map | Array de dashcards (cards posicionadas) |
  | parameters | []map | Filtros del dashboard |
  | tabs | []map | Tabs del dashboard |
  | collection_id | float64 | Coleccion contenedora |
  | archived | bool | Archivado |

  ### Estructura de cada dashcard

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | id | float64 | ID del dashcard (positivo) |
  | card_id | float64 | ID de la card/pregunta asociada |
  | card | map | Objeto card completo |
  | size_x | float64 | Ancho en grid (1-18) |
  | size_y | float64 | Alto en grid |
  | col | float64 | Columna en grid (0-based) |
  | row | float64 | Fila en grid (0-based) |
  | dashboard_tab_id | float64 | Tab al que pertenece (null = sin tabs) |
  | parameter_mappings | []map | Mapeo de filtros a la card |
  | visualization_settings | map | Settings de visualizacion |

  Usar estos datos para construir el payload de MetabaseUpdateDashboard.

code:
  package infra

  import "fmt"

  // MetabaseGetDashboard obtiene un dashboard completo de Metabase incluyendo sus cards.
  func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error) {
  	path := fmt.Sprintf("/api/dashboard/%d", dashboardID)

  	result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return nil, fmt.Errorf("metabase get dashboard %d: %w", dashboardID, err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"dashboardID","desc":"ID del dashboard"}],"output":"mapa con dashboard completo incluyendo dashcards, tabs, parametros"}


id: metabase_get_user_go_infra
name: metabase_get_user
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error)
description: Obtiene los detalles de un usuario de Metabase por su ID numerico. Endpoint: GET /api/user/:id.
tags: ["metabase","user","get","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_get_user.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f4ab4b962dc122cde8cfe823255417da01d6cb2190219b46ea5eb673f823730a
example:
  ```go
  user, err := MetabaseGetUser(client, 1)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(user["email"], user["first_name"])
  ```

notes:
  Retorna el objeto usuario completo como map. Error 404 si el ID no existe.

  ### Campos del usuario retornado

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | id | float64 | ID numerico |
  | email | string | Email |
  | first_name | string | Nombre |
  | last_name | string | Apellido |
  | is_superuser | bool | Es admin |
  | is_active | bool | Esta activo |
  | common_name | string | Nombre completo |
  | date_joined | string | Fecha de creacion |
  | last_login | string | Ultimo login |
  | group_ids | []float64 | IDs de grupos |
  | locale | string | Locale del usuario |

code:
  package infra

  import "fmt"

  // MetabaseGetUser obtiene un usuario de Metabase por su ID.
  func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error) {
  	path := fmt.Sprintf("/api/user/%d", userID)

  	result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return nil, fmt.Errorf("metabase get user %d: %w", userID, err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"userID","desc":"ID del usuario"}],"output":"mapa con detalles del usuario (email, nombre, grupo, permisos, etc.)"}


id: metabase_list_cards_go_infra
name: metabase_list_cards
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error)
tags: ["metabase","card","question","list","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_list_cards.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 87fa81e68bd21665cf2843762ee756ff80d02c97081b8bb1c9c7a7cf7064b378
description:
  Lista preguntas/cards de Metabase con filtro opcional. Retorna array de cards. Endpoint: GET /api/card.

example:
  ```go
  // Listar todas las cards
  cards, err := MetabaseListCards(client, "all", 0)

  // Solo mis preguntas
  cards, err := MetabaseListCards(client, "mine", 0)

  // Cards de una database especifica
  cards, err := MetabaseListCards(client, "database", 1)

  // Cards archivadas
  cards, err := MetabaseListCards(client, "archived", 0)
  ```

notes:
  ### Parametros para un LLM

  | Parametro | Tipo | Requerido | Descripcion |
  |-----------|------|-----------|-------------|
  | client | MetabaseClient | si | Cliente autenticado |
  | filter | string | no | "all", "mine", "fav", "archived", "recent", "popular", "database", "table". Vacio = todas |
  | modelID | int | no | ID de database/tabla. Solo aplica con filter "database" o "table". 0 = ignorar |

  No tiene paginacion con offset/limit. Retorna todas las cards que coinciden.

  ### Campos principales de cada card

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | id | float64 | ID numerico de la card |
  | name | string | Nombre de la pregunta |
  | description | string | Descripcion |
  | display | string | Tipo de visualizacion (table, bar, line, pie, etc.) |
  | collection_id | float64 | ID de la coleccion/carpeta |
  | database_id | float64 | ID de la database |
  | creator_id | float64 | ID del creador |
  | archived | bool | Esta archivada |
  | dataset_query | map | Query de la card (native o structured) |

code:
  package infra

  import "fmt"

  // MetabaseListCards lista preguntas/cards de Metabase.
  // filter: "all", "mine", "fav", "archived", "recent", "popular", "database", "table" (vacio = todas).
  // modelID: ID de database o tabla cuando filter es "database" o "table" (0 = ignorar).
  func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error) {
  	path := "/api/card"
  	sep := "?"
  	if filter != "" {
  		path += sep + "f=" + filter
  		sep = "&"
  	}
  	if modelID > 0 {
  		path += fmt.Sprintf("%smodel_id=%d", sep, modelID)
  	}

  	result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return nil, fmt.Errorf("metabase list cards: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"filter","desc":"filtro opcional (all, mine, fav, archived, recent, popular, database, table)"},{"name":"modelID","desc":"ID de database/tabla para filtros database/table (0 = ignorar)"}],"output":"slice de maps con detalles de cada card (ID, nombre, query, visualizacion)"}


id: metabase_list_dashboards_go_infra
name: metabase_list_dashboards
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error)
tags: ["metabase","dashboard","list","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_list_dashboards.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 806f9c4e2c3dfe2b64eeb36e7aaba01804e04a6c62e5b042ef70cee396e3813e
description:
  Lista dashboards de Metabase con filtro opcional. Retorna array de dashboards resumidos (sin dashcards). Endpoint: GET /api/dashboard.

example:
  ```go
  // Listar todos los dashboards
  dashboards, err := MetabaseListDashboards(client, "all")

  // Solo mis dashboards
  dashboards, err := MetabaseListDashboards(client, "mine")

  // Dashboards archivados
  dashboards, err := MetabaseListDashboards(client, "archived")
  ```

notes:
  ### Parametros para un LLM

  | Parametro | Tipo | Requerido | Descripcion |
  |-----------|------|-----------|-------------|
  | client | MetabaseClient | si | Cliente autenticado |
  | filter | string | no | "all", "mine", "archived". Vacio = todas |

  Retorna dashboards resumidos (sin cards). Para ver las cards de un dashboard, usar MetabaseGetDashboard.

  ### Campos principales de cada dashboard

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | id | float64 | ID del dashboard |
  | name | string | Nombre |
  | description | string | Descripcion |
  | collection_id | float64 | Coleccion contenedora |
  | creator_id | float64 | ID del creador |
  | archived | bool | Archivado |
  | created_at | string | Fecha creacion |

code:
  package infra

  import "fmt"

  // MetabaseListDashboards lista dashboards de Metabase.
  // filter: "all", "mine" o "archived" (vacio = todas).
  func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error) {
  	path := "/api/dashboard"
  	if filter != "" {
  		path += "?f=" + filter
  	}

  	result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return nil, fmt.Errorf("metabase list dashboards: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"filter","desc":"filtro opcional (all, mine, archived o vacio para todas)"}],"output":"slice de maps con resumen de cada dashboard (sin dashcards completas)"}


id: metabase_list_users_go_infra
name: metabase_list_users
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","user","list","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_list_users.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 087b29382631aba55ab030e4da4809ed0eece23a95ef500228d1b6f7bf53291e
signature:
  func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error)

description:
  Lista usuarios de una instancia Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user. Requiere permisos de superusuario.

example:
  ```go
  client, _ := MetabaseAuth("http://localhost:3000", "admin@example.com", "pass")

  // Listar todos los usuarios activos
  users, err := MetabaseListUsers(client, "active", "", 0, 0)

  // Buscar usuario por email
  users, err := MetabaseListUsers(client, "", "john@", 10, 0)

  // Listar desactivados
  users, err := MetabaseListUsers(client, "deactivated", "", 25, 0)
  ```

notes:
  Retorna un map con la estructura paginada de Metabase:
  - `data`: array de objetos usuario (id, email, first_name, last_name, is_superuser, etc.)
  - `total`: numero total de usuarios que coinciden
  - `limit`: tamanio de pagina usado
  - `offset`: offset usado

  ### Parametros para un LLM

  | Parametro | Tipo | Requerido | Descripcion |
  |-----------|------|-----------|-------------|
  | client | MetabaseClient | si | Cliente autenticado |
  | status | string | no | "active" (default), "deactivated", "all" |
  | query | string | no | Filtro por nombre o email |
  | limit | int | no | Tamanio de pagina (0 = default Metabase) |
  | offset | int | no | Offset para paginacion (0 = inicio) |

  ### Campos del usuario retornado

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | id | float64 | ID numerico del usuario |
  | email | string | Email unico |
  | first_name | string | Nombre |
  | last_name | string | Apellido |
  | is_superuser | bool | Es admin |
  | is_active | bool | Esta activo |
  | common_name | string | Nombre completo |
  | last_login | string | Fecha ultimo login |

code:
  package infra

  import "fmt"

  // MetabaseListUsers lista usuarios de Metabase con filtros opcionales.
  // status: "active", "deactivated" o "all" (vacio = "active").
  // query: filtro por nombre o email (vacio = sin filtro).
  // limit/offset: paginacion (0 = valores por defecto de Metabase).
  func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error) {
  	path := "/api/user?"
  	if status != "" {
  		path += "status=" + status + "&"
  	}
  	if query != "" {
  		path += "query=" + query + "&"
  	}
  	if limit > 0 {
  		path += fmt.Sprintf("limit=%d&", limit)
  	}
  	if offset > 0 {
  		path += fmt.Sprintf("offset=%d&", offset)
  	}

  	result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
  	if err != nil {
  		return nil, fmt.Errorf("metabase list users: %w", err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado con permisos admin"},{"name":"status","desc":"filtro por estado (active, deactivated, all o vacio)"},{"name":"query","desc":"filtro por nombre o email (vacio para sin filtro)"},{"name":"limit","desc":"tamanio de pagina (0 = default de Metabase)"},{"name":"offset","desc":"offset para paginacion (0 = inicio)"}],"output":"mapa con data (slice de usuarios), total, limit, offset"}


id: metabase_update_card_go_infra
name: metabase_update_card
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","card","question","update","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_update_card.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 24fe499f3ca6b1541be475c9ba77e0f5081a458395bb60a812e1ac38af54a8c8
signature:
  func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error)

description:
  Actualiza campos de una card/pregunta en Metabase. Solo se modifican los campos incluidos en el map. Endpoint: PUT /api/card/:id.

example:
  ```go
  // Cambiar nombre y descripcion
  card, err := MetabaseUpdateCard(client, 42, map[string]any{
      "name":        "Updated Revenue Chart",
      "description": "Now includes refunds",
  })

  // Archivar una card (soft-delete)
  card, err := MetabaseUpdateCard(client, 42, map[string]any{
      "archived": true,
  })

  // Mover a otra coleccion
  card, err := MetabaseUpdateCard(client, 42, map[string]any{
      "collection_id": 10,
  })

  // Cambiar la query SQL
  card, err := MetabaseUpdateCard(client, 42, map[string]any{
      "dataset_query": map[string]any{
          "database": 1,
          "type":     "native",
          "native":   map[string]any{"query": "SELECT * FROM users LIMIT 100"},
      },
  })
  ```

notes:
  ### Campos actualizables

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | name | string | Nombre de la pregunta |
  | description | string | Descripcion |
  | display | string | Tipo de visualizacion |
  | dataset_query | map | Query SQL o MBQL |
  | visualization_settings | map | Config de visualizacion |
  | collection_id | int | Mover a otra coleccion |
  | archived | bool | Archivar/desarchivar (soft-delete) |
  | enable_embedding | bool | Habilitar embedding publico |
  | embedding_params | map | Parametros de embedding |

  Solo incluir los campos que se quieren cambiar.
  Para eliminar permanentemente usar MetabaseDeleteCard. Para soft-delete usar archived: true.

code:
  package infra

  import "fmt"

  // MetabaseUpdateCard actualiza campos de una card/pregunta en Metabase.
  // fields es un map con los campos a actualizar.
  // Campos comunes: name, description, display, dataset_query, visualization_settings,
  // collection_id, archived, enable_embedding.
  func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error) {
  	path := fmt.Sprintf("/api/card/%d", cardID)

  	result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
  	if err != nil {
  		return nil, fmt.Errorf("metabase update card %d: %w", cardID, err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"cardID","desc":"ID de la card/pregunta a actualizar"},{"name":"fields","desc":"mapa con los campos a modificar (name, description, display, dataset_query, archived, etc.)"}],"output":"mapa con los detalles de la card actualizada"}


id: metabase_update_dashboard_go_infra
name: metabase_update_dashboard
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","dashboard","update","cards","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_update_dashboard.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 319678921fda2088275e1e6d0df339dd79c6170c1dcc1102876de502f32b203d
signature:
  func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error)

description:
  Actualiza un dashboard en Metabase incluyendo metadata, cards y tabs. El campo dashcards representa el estado completo deseado: cards nuevas con ID negativo, existentes con ID positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id.

example:
  ```go
  // Cambiar nombre
  MetabaseUpdateDashboard(client, 1, map[string]any{
      "name": "Updated Dashboard",
  })

  // Agregar una card al dashboard
  // Primero obtener las dashcards existentes
  dash, _ := MetabaseGetDashboard(client, 1)
  existingCards := dash["dashcards"].([]any)

  // Construir nuevo array con las existentes + la nueva
  dashcards := make([]map[string]any, 0)
  for _, dc := range existingCards {
      dashcards = append(dashcards, dc.(map[string]any))
  }
  // Agregar nueva card (ID negativo = nueva)
  dashcards = append(dashcards, map[string]any{
      "id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0,
  })

  MetabaseUpdateDashboard(client, 1, map[string]any{
      "dashcards": dashcards,
  })

  // Archivar dashboard (soft-delete)
  MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
  ```

notes:
  ### Gestion de dashcards (IMPORTANTE)

  El array `dashcards` representa el **estado completo deseado** del dashboard:

  | Accion | Como hacerlo |
  |--------|-------------|
  | Agregar card | Incluir con **ID negativo** (-1, -2, etc.) |
  | Actualizar card | Incluir con su **ID positivo** existente |
  | Eliminar card | **Omitir** del array |
  | No cambiar cards | No incluir el campo dashcards |

  **Flujo tipico para agregar una card:**
  1. `MetabaseGetDashboard` para obtener dashcards existentes
  2. Copiar las existentes al nuevo array
  3. Agregar la nueva con ID negativo
  4. Enviar el array completo

  ### Estructura de una dashcard

  ```go
  map[string]any{
      "id":                      -1,              // negativo = nueva, positivo = existente
      "card_id":                 42,              // ID de la card/pregunta
      "size_x":                  6,               // ancho (1-18)
      "size_y":                  4,               // alto
      "col":                     0,               // columna (0-based)
      "row":                     0,               // fila (0-based)
      "dashboard_tab_id":        nil,             // tab (nil = sin tabs)
      "parameter_mappings":      []map[string]any{}, // mapeo de filtros
      "visualization_settings":  map[string]any{},   // settings custom
  }
  ```

  ### Gestion de tabs

  ```go
  map[string]any{
      "tabs": []map[string]any{
          {"id": 1, "name": "Overview"},           // tab existente
          {"id": -1, "name": "Details"},           // tab nuevo (ID negativo)
      },
  }
  ```

  ### Campos actualizables

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | name | string | Nombre del dashboard |
  | description | string | Descripcion |
  | archived | bool | Archivar/desarchivar |
  | dashcards | []map | Estado completo de cards |
  | tabs | []map | Tabs del dashboard |
  | parameters | []map | Filtros del dashboard |
  | collection_id | int | Mover a otra coleccion |

code:
  package infra

  import "fmt"

  // MetabaseUpdateDashboard actualiza un dashboard en Metabase.
  // fields puede incluir metadata del dashboard Y/O la lista completa de dashcards y tabs.
  //
  // Para gestionar cards en el dashboard, incluir "dashcards" en fields:
  //   - Agregar card: incluirla con ID negativo (ej: -1, -2)
  //   - Actualizar card: incluirla con su ID positivo existente
  //   - Eliminar card: omitirla del array (el array es el estado deseado completo)
  //
  // Campos comunes: name, description, archived, parameters, dashcards, tabs, collection_id.
  func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error) {
  	path := fmt.Sprintf("/api/dashboard/%d", dashboardID)

  	result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
  	if err != nil {
  		return nil, fmt.Errorf("metabase update dashboard %d: %w", dashboardID, err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado"},{"name":"dashboardID","desc":"ID del dashboard a actualizar"},{"name":"fields","desc":"mapa con campos a modificar (name, description, dashcards, tabs, archived, etc.)"}],"output":"mapa con los detalles del dashboard actualizado"}


id: metabase_update_user_go_infra
name: metabase_update_user
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
tags: ["metabase","user","update","api"]
uses_functions: []
uses_types: ["MetabaseClient_go_infra"]
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/metabase_update_user.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 219f4406a5e9ab48207f662a753bbd5ee03ee2322bab227170d4f88072dcff89
signature:
  func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error)

description:
  Actualiza campos de un usuario en Metabase. Solo se modifican los campos incluidos en el map. Requiere permisos de superusuario. Endpoint: PUT /api/user/:id.

example:
  ```go
  // Cambiar nombre
  user, err := MetabaseUpdateUser(client, 5, map[string]any{
      "first_name": "Jane",
      "last_name":  "Smith",
  })

  // Promover a admin
  user, err := MetabaseUpdateUser(client, 5, map[string]any{
      "is_superuser": true,
  })

  // Cambiar grupos
  user, err := MetabaseUpdateUser(client, 5, map[string]any{
      "group_ids": []int{1, 3, 5},
  })
  ```

notes:
  ### Campos actualizables

  | Campo | Tipo | Descripcion |
  |-------|------|-------------|
  | first_name | string | Nombre |
  | last_name | string | Apellido |
  | email | string | Email (debe ser unico) |
  | is_superuser | bool | Permisos de admin |
  | group_ids | []int | IDs de grupos del usuario |
  | locale | string | Locale (ej: "es", "en") |
  | login_attributes | map | Atributos para sandboxing |

  Solo incluir los campos que se quieren cambiar. Los demas se mantienen sin modificar.
  Retorna el objeto usuario actualizado.

code:
  package infra

  import "fmt"

  // MetabaseUpdateUser actualiza campos de un usuario en Metabase.
  // fields es un map con los campos a actualizar. Campos validos:
  // first_name, last_name, email, is_superuser, group_ids, locale, login_attributes.
  func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error) {
  	path := fmt.Sprintf("/api/user/%d", userID)

  	result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
  	if err != nil {
  		return nil, fmt.Errorf("metabase update user %d: %w", userID, err)
  	}

  	return result, nil
  }

params_schema:
  {"params":[{"name":"client","desc":"cliente MetabaseClient autenticado con permisos admin"},{"name":"userID","desc":"ID del usuario a actualizar"},{"name":"fields","desc":"mapa con campos a modificar (first_name, last_name, email, is_superuser, group_ids, locale)"}],"output":"mapa con los detalles del usuario actualizado"}


id: nordvpn_container_run_go_infra
name: nordvpn_container_run
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func NordVPNContainerRun(opts NordVPNContainerRunOpts) (string, error)
tags: ["vpn","nordvpn","docker","container","run","infra","network"]
uses_functions: ["docker_run_container_go_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt"]
tests: []
file_path: functions/infra/nordvpn_container_run.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: fb5d1b6d4bbb8307f8cda0b3bee91f7abd05cce404f3a0321613702b77713ac0
description:
  Ejecuta un container Docker cuyo trafico pasa por el gateway NordVPN usando --network=container:<gateway>. El container hereda la IP y tunel VPN del gateway.

example:
  ```go
  // Verificar IP desde VPN
  id, err := NordVPNContainerRun(NordVPNContainerRunOpts{
      Image:   "curlimages/curl",
      Cmd:     []string{"https://api.ipify.org"},
      Remove:  true,
      Gateway: "nordvpn",
  })

  // Ejecutar scraper bajo VPN
  id, err := NordVPNContainerRun(NordVPNContainerRunOpts{
      Image:   "my-scraper:latest",
      Env:     map[string]string{"TARGET_URL": "https://example.com"},
      Volumes: []string{"/tmp/output:/output"},
      Detach:  true,
      Name:    "scraper-vpn",
  })

  // Navegador headless bajo VPN
  id, err := NordVPNContainerRun(NordVPNContainerRunOpts{
      Image:   "chromedp/headless-shell",
      Detach:  true,
      Name:    "chrome-vpn",
  })
  ```

notes:
  Requiere que el container gateway NordVPN este corriendo (usar `NordVPNContainerStart` primero). El container cliente no necesita capabilities especiales — hereda la red del gateway. Con `--network=container:X` el container no puede exponer puertos propios; los puertos deben mapearse en el gateway.

code:
  package infra

  import (
  	"fmt"
  )

  // NordVPNContainerRunOpts opciones para ejecutar un container a traves del gateway NordVPN.
  type NordVPNContainerRunOpts struct {
  	Image   string            // Imagen Docker a ejecutar (obligatorio)
  	Cmd     []string          // Comando a ejecutar en el container
  	Env     map[string]string // Variables de entorno
  	Volumes []string          // Bind mounts
  	Name    string            // Nombre del container (opcional)
  	Gateway string            // Nombre del container NordVPN gateway (default: "nordvpn")
  	Detach  bool              // Ejecutar en background
  	Remove  bool              // Eliminar al terminar (--rm)
  }

  // NordVPNContainerRun ejecuta un container Docker cuyo trafico de red
  // pasa por el container gateway NordVPN usando --network=container:<gateway>.
  // Devuelve el ID del container creado.
  func NordVPNContainerRun(opts NordVPNContainerRunOpts) (string, error) {
  	if opts.Image == "" {
  		return "", fmt.Errorf("image required")
  	}
  	if opts.Gateway == "" {
  		opts.Gateway = "nordvpn"
  	}

  	id, err := DockerRunContainer(opts.Image, DockerRunOpts{
  		Name:    opts.Name,
  		Env:     opts.Env,
  		Volumes: opts.Volumes,
  		Detach:  opts.Detach,
  		Remove:  opts.Remove,
  		Network: "container:" + opts.Gateway,
  		Cmd:     opts.Cmd,
  	})
  	if err != nil {
  		return "", fmt.Errorf("nordvpn container run %s: %w", opts.Image, err)
  	}

  	return id, nil
  }

params_schema:
  {"params":[{"name":"opts","desc":"estructura NordVPNContainerRunOpts con Image, Cmd, Env, Volumes, Detach, Name, Gateway"}],"output":"ID del contenedor ejecutado bajo VPN"}


id: nordvpn_container_start_go_infra
name: nordvpn_container_start
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func NordVPNContainerStart(opts NordVPNContainerOpts) (string, error)
tags: ["vpn","nordvpn","docker","container","gateway","infra","network"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","strings","time"]
tests: []
file_path: functions/infra/nordvpn_container_start.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 24548d4d425be87612deb564d9f77e20bcb1befccc2cd23bc09042367d725318
description:
  Levanta un container Docker con NordVPN como gateway de red. Otros containers pueden rutear su trafico a traves de este con --network=container:<name>. Espera hasta 30s a que el tunel este activo.

uses_functions:
  ["docker_run_container_go_infra","docker_remove_container_go_infra","docker_container_logs_go_infra"]

example:
  ```go
  id, err := NordVPNContainerStart(NordVPNContainerOpts{
      Token:   os.Getenv("NORDVPN_TOKEN"),
      Country: "Spain",
      City:    "Madrid",
  })
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("VPN gateway:", id)

  // Ahora otros containers pueden usar la VPN:
  // docker run --network=container:nordvpn curlimages/curl https://api.ipify.org
  ```

notes:
  Usa la imagen `ghcr.io/bubuntux/nordvpn`. Requiere un token de acceso NordVPN (obtener con `nordvpn token` desde CLI o desde la web de NordVPN). Limpia containers previos con el mismo nombre automaticamente. El protocolo por defecto es NordLynx (WireGuard).

code:
  package infra

  import (
  	"fmt"
  	"strings"
  	"time"
  )

  // NordVPNContainerOpts opciones para el container gateway NordVPN.
  type NordVPNContainerOpts struct {
  	Token    string // Token de acceso NordVPN (obligatorio)
  	Country  string // Pais al que conectar (opcional, ej: "Spain")
  	City     string // Ciudad (opcional, ej: "Madrid")
  	Protocol string // "NordLynx" o "OpenVPN" (default: NordLynx)
  	Name     string // Nombre del container (default: "nordvpn")
  }

  // NordVPNContainerStart levanta un container Docker con NordVPN como gateway.
  // Otros containers pueden usar su red con --network=container:<name>.
  // Espera hasta que el tunel este activo o timeout de 30s.
  func NordVPNContainerStart(opts NordVPNContainerOpts) (string, error) {
  	if opts.Token == "" {
  		return "", fmt.Errorf("nordvpn token required")
  	}
  	if opts.Name == "" {
  		opts.Name = "nordvpn"
  	}
  	if opts.Protocol == "" {
  		opts.Protocol = "NordLynx"
  	}

  	env := map[string]string{
  		"TOKEN":      opts.Token,
  		"TECHNOLOGY": opts.Protocol,
  	}
  	if opts.Country != "" {
  		connect := opts.Country
  		if opts.City != "" {
  			connect += " " + opts.City
  		}
  		env["CONNECT"] = connect
  	}

  	// Limpiar container previo con el mismo nombre si existe
  	_ = DockerRemoveContainer(opts.Name, true)

  	id, err := DockerRunContainer("ghcr.io/bubuntux/nordvpn", DockerRunOpts{
  		Name:   opts.Name,
  		Env:    env,
  		Detach: true,
  		CapAdd: []string{"NET_ADMIN", "NET_RAW"},
  	})
  	if err != nil {
  		return "", fmt.Errorf("nordvpn container start: %w", err)
  	}

  	// Esperar a que el tunel este activo
  	for i := 0; i < 30; i++ {
  		time.Sleep(1 * time.Second)
  		logs, logErr := DockerContainerLogs(opts.Name, 20)
  		if logErr != nil {
  			continue
  		}
  		if strings.Contains(logs, "Connected") || strings.Contains(logs, "connected") {
  			return id, nil
  		}
  		if strings.Contains(logs, "error") || strings.Contains(logs, "failed") {
  			return id, fmt.Errorf("nordvpn connection failed, check logs: docker logs %s", opts.Name)
  		}
  	}

  	return id, fmt.Errorf("nordvpn connection timeout after 30s, check logs: docker logs %s", opts.Name)
  }

params_schema:
  {"params":[{"name":"opts","desc":"estructura NordVPNContainerOpts con Token, Country, City, Protocol"}],"output":"ID del contenedor gateway NordVPN"}


