---
id: 0034
title: Port de los 5 enrichers de sistema a Go
status: pending
priority: medium
created: 2026-05-02
depends_on: [0033]
---

## Contexto

Tras 0033 el dispatcher acepta `lang: go` y los binarios se
distribuyen junto al `.exe` Windows. Los 5 enrichers de sistema
(`extract_domain`, `fetch_webpage`, `extract_links`,
`extract_text_entities`, `web_search`) son hoy Python — funcionan,
estan testeados y son el flujo OSINT canonico de la app. Portarlos
a Go nos da:

- Cero dependencia de Python para el flujo base. El runtime
  embebido pasa a ser **opcional** (solo apps con enrichers
  custom Python lo necesitan).
- ~10x menos arranque por job (Python cold ~120 ms, Go ~10 ms).
- Reuso directo de funciones Go testeadas del registry (extract_iocs,
  extract_urls, normalize_url, html_to_markdown). Esto cierra el
  bucle del registry: lo que se testea en Go se ejecuta en Go.
- Distribucion: el `.exe` + binarios `enrichers/<id>/run.exe` son
  ~25 MB totales; sin runtime Python si no hace falta.

Los tests pytest existentes **no cambian** — testean el wire
protocol (stdin JSON / stdout JSON / exit code), no la
implementacion. Cada port intercambia el binario pero deja el
contrato intacto.

## Funciones del registry necesarias

Los 5 enrichers comparten una decena de funciones del dominio
`cybersecurity` y `core`. Verificar que existen en Go con `fn search`
antes de portar; si falta alguna, anadirla al registry primero.

| Funcion Python actual | Equivalente Go que hace falta | Estado |
|---|---|---|
| `cybersecurity.normalize_url` | `normalize_url_go_cybersecurity` | revisar |
| `core.html_to_markdown` | `html_to_markdown_go_core` | crear si no esta |
| `cybersecurity.extract_urls` | `extract_urls_go_cybersecurity` | revisar |
| `cybersecurity.extract_iocs` | `extract_iocs_go_cybersecurity` | crear si no esta |

`fn check params` + `fn list -d cybersecurity --lang go` muestra que
hay y que falta. Las que falten se crean primero (issue separada
por funcion si la complejidad lo justifica, ej. `html_to_markdown`
necesita un parser HTML — `golang.org/x/net/html` resuelve).

## Layout por enricher

```
enrichers/<id>/
  manifest.yaml         # actualizado: lang: go
  main.go               # entry point con main()
  go.mod                # dependencias propias
  run                   # binario Linux (gitignored, generado)
  run.exe               # binario Windows (gitignored, generado)
  build.sh              # cross-compile recipe
  run.py                # ELIMINADO tras port + tests verde
```

Ejemplo de `build.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
GOOS=linux   GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o run     .
GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o run.exe .
```

Hook al build de la app: `cpp/CMakeLists.txt` (o `compile.sh`) corre
`build.sh` de cada enricher Go antes de copiar la app a Desktop. Idem
en linux build.

## Estructura del `main.go` (template)

```go
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "os"

    // Funciones del registry — paths relativos al repo root.
    "fn_registry/cybersecurity/extractiocs"
    "fn_registry/cybersecurity/extracturls"
)

type ctxIn struct {
    NodeID       string                 `json:"node_id"`
    NodeName     string                 `json:"node_name"`
    NodeType     string                 `json:"node_type"`
    Metadata     map[string]any         `json:"metadata"`
    OpsDBPath    string                 `json:"ops_db_path"`
    AppDir       string                 `json:"app_dir"`
    CacheDir     string                 `json:"cache_dir"`
    RegistryRoot string                 `json:"registry_root"`
    Params       map[string]any         `json:"params"`
}

type ctxOut struct {
    EntitiesAdded  int    `json:"entities_added"`
    RelationsAdded int    `json:"relations_added"`
    Error          string `json:"error,omitempty"`
    // ... campos especificos del enricher
}

func progress(p float64, stage string) {
    fmt.Fprintf(os.Stderr, "PROGRESS:%.2f %s\n", p, stage)
}

func main() {
    raw, _ := io.ReadAll(os.Stdin)
    var in ctxIn
    if err := json.Unmarshal(raw, &in); err != nil {
        fmt.Fprintln(os.Stderr, "stdin not valid JSON:", err)
        os.Exit(2)
    }
    out, code := run(in)
    enc, _ := json.Marshal(out)
    fmt.Println(string(enc))
    os.Exit(code)
}

func run(in ctxIn) (ctxOut, int) {
    // Logica especifica del enricher, devolviendo (resumen, exit code).
}
```

## Plan de port (orden y por que)

### Fase 1 — `extract_domain` (1 sesion)

El mas simple: regex puro, sin red, sin parser HTML. Sirve de
**referencia canonica** para los demas. Confirma toda la cadena de
build + dispatcher + tests pytest sin tocar dependencias externas.

### Fase 2 — `web_search` (1-2 sesiones)

Recien escrito en Python, todavia caliente. Portar ahora minimiza el
trabajo de mantenerlo en dos sitios. Dependencias: `net/http` (stdlib),
parser HTML con `golang.org/x/net/html`.

### Fase 3 — `extract_links` (1 sesion)

Lee markdown del cache, extract_urls. Si `extract_urls_go_cybersecurity`
existe, port directo; si no, escribir la funcion antes (regex de URLs).

### Fase 4 — `extract_text_entities` (1-2 sesiones)

Si `extract_iocs_go_cybersecurity` no existe, crear primero como
funcion del registry — es el plato fuerte del port.

### Fase 5 — `fetch_webpage` (2 sesiones)

El mas complejo: HTTP con redirects, decode de encoding, parse HTML
a markdown, escritura de cache. Lo dejamos para el final cuando ya
tenemos las primitivas Go probadas.

## Tests

**Cero cambios necesarios** en los tests pytest si el dispatcher
detecta correctamente el binario Go. El stub `tests/_stubs/requests.py`
deja de aplicar para los enrichers Go — en su lugar se inyecta un
servidor HTTP local con `httptest.Server` desde un test Go nativo
**adicional** (no sustituto):

```go
// enrichers/web_search/main_test.go
func TestWebSearchParsesDDGFixture(t *testing.T) {
    srv := httptest.NewServer(http.HandlerFunc(func(w, r) {
        body, _ := os.ReadFile("../../tests/fixtures/ddg_results.html")
        w.Write(body)
    }))
    defer srv.Close()
    // ... ctx con DDGEndpoint sobreescrito a srv.URL ...
}
```

Los tests pytest se mantienen como **integration tests del wire
protocol** y los tests Go cubren la logica unitaria. Total post-port:

- 16 tests pytest (sin cambios) — wire protocol.
- ~20 tests Go nuevos — logica de cada enricher.

## Limpieza tras port

Por enricher, una vez los 16+N tests estan verde:
1. Borrar `run.py`.
2. Actualizar `manifest.yaml`: `lang: go`.
3. Actualizar `app.md` `python_runtime_deps` quitando lo que ya no se
   use (si extract_text_entities ya no necesita `requests`, fuera).

Cuando los 5 esten portados, decidir:
- Si `python_runtime: true` sigue siendo util → mantener (custom
  enrichers, prototipado).
- Si nadie escribe Python custom → marcar `python_runtime: false` y
  el `.exe` deja de embeber Python por completo. Re-habilitable con
  un solo flag.

## Definicion de hecho

- 5 enrichers con `lang: go` y binarios precompilados para
  Linux + Windows en el build pipeline.
- 16 tests pytest pasan contra los binarios Go (mismo wire
  protocol).
- Tests Go nativos cubren parsing/regex/IO de cada enricher.
- `graph_explorer.exe` distribuido a Desktop sin runtime Python
  ejecuta el flujo OSINT completo (search → fetch → extract).
- `python_runtime: true` queda como flag opcional, no obligatorio.

## Riesgos y mitigaciones

| Riesgo | Mitigacion |
|---|---|
| Falta `extract_iocs` o `html_to_markdown` en Go | Issue dependiente que las crea primero. Marcar este 0034 como bloqueado hasta que existan. |
| Diferencia de comportamiento Python vs Go (regex, normalizacion HTML) | Tests pytest comparten fixtures de input — si Go produce salida distinta a Python, falla. Iteramos hasta paridad. |
| Cross-compile de Go con cgo | Los enrichers no necesitan cgo. Build estatico simple `CGO_ENABLED=0`. |
| Mantener dos implementaciones durante el port | NO se mantienen dos. Cada enricher se porta en una rama corta, tests verde, merge, eliminacion del `run.py`. Nada de toggles. |
| Echo escribiendo enrichers nuevos durante el port | Echo escribe Python custom — eso vive feliz junto a los Go portados (gracias al dispatcher de 0033). Sin conflicto. |

## Fuera de alcance

- Port de enrichers de fase 2 (browser session, screenshot, login).
  Esos viven en `lang: python` con Playwright porque la libreria Go
  equivalente (`chromedp`) duplica esfuerzo sin ganancia clara.
  Justamente el caso de uso ideal del runtime embebido.
- Reescribir el sistema de jobs en Go (issue futura si el panel C++
  se queda corto).
- Ports en otros lenguajes (Rust, Zig) — no aporta ahora.
