# Estandar de Ejecucion: YAML + Exit Codes + Hooks

Contrato comun para apps, pipelines y automatizaciones del fn-registry. Define como declarar flujos en YAML, que exit codes devolver, como reportar resultados y como integrarse con dag_engine y operations.db.

---

## Motivacion

Tenemos dos patrones que ya funcionan:

- **dag_engine** (`apps/dag_engine/`): orquesta DAGs con `command`/`function`, usa exit codes para decidir flujo, soporta `handlers` (init/success/failure/exit) y scheduling cron via `schedule:`.
- **script_navegador**: ejecuta pasos tipados (CDP actions) desde YAML, registra todo en operations.db, sale con 0/1.

Ambos comparten la misma idea: **YAML declara el flujo, exit code señala el resultado, logs estructurados permiten analisis posterior**. Este documento formaliza ese contrato para que cualquier app nueva lo siga desde el inicio.

---

## 1. Estructura YAML

Toda app que ejecute un flujo debe leer un YAML con esta estructura minima:

```yaml
# Cabecera obligatoria
name: nombre_del_flujo
description: "Que hace este flujo"  # opcional pero recomendado

# Variables de entorno disponibles para todos los pasos
env:                                 # opcional
  KEY: value

# Pasos del flujo
steps:
  - name: paso_1                     # identificador unico dentro del flujo
    action: tipo_de_accion           # dispatch key (navigate, command, query, etc.)
    # ... parametros especificos del action
    continue_on_error: false         # default: false
    timeout_ms: 30000                # default: 30s, 0 = sin timeout
    depends: []                      # IDs de pasos previos (vacio = secuencial)

# Hooks opcionales
hooks:
  on_success: "comando o script"     # se ejecuta si exit code = 0
  on_failure: "comando o script"     # se ejecuta si exit code = 1
  on_partial: "comando o script"     # se ejecuta si exit code = 2
```

### Campos de la cabecera

| Campo | Requerido | Descripcion |
|-------|-----------|-------------|
| `name` | si | Identificador del flujo (snake_case) |
| `description` | no | Descripcion legible |
| `env` | no | Variables de entorno (mapa key-value) |
| `steps` | si | Lista de pasos a ejecutar |
| `hooks` | no | Comandos a ejecutar segun resultado |

### Campos de cada step

| Campo | Requerido | Default | Descripcion |
|-------|-----------|---------|-------------|
| `name` | si | — | ID unico del paso dentro del flujo |
| `action` | si | — | Tipo de accion (cada app define sus propios actions) |
| `continue_on_error` | no | `false` | Si `true`, un fallo no detiene el flujo |
| `timeout_ms` | no | `30000` | Timeout del paso en ms (0 = sin timeout) |
| `depends` | no | `[]` | Pasos que deben completarse antes |

Los campos adicionales dependen del `action` y los define cada app/dominio.

### Actions por dominio

Cada app registra sus actions validos. Ejemplos:

| Dominio | Actions | App de referencia |
|---------|---------|-------------------|
| navegacion | `navigate`, `click`, `type`, `wait`, `screenshot`, `evaluate`, `get_html`, `wait_load`, `sleep` | script_navegador |
| shell | `command`, `script` | dag_engine / apps genericas |
| registry | `function`, `args` | dag_engine (invoca `fn run <id>` y captura `function_id` en `dag_step_results`) |
| database | `query`, `migrate`, `backup` | futuras apps |
| http | `request`, `assert_status`, `extract` | futuras apps |

---

## 2. Exit Codes

Toda app/script que siga este estandar DEBE salir con uno de estos tres codigos:

| Codigo | Constante | Significado |
|--------|-----------|-------------|
| `0` | `EXIT_SUCCESS` | Todos los pasos completaron sin error |
| `1` | `EXIT_FAILURE` | Al menos un paso fallo y el flujo aborto |
| `2` | `EXIT_PARTIAL` | Algunos pasos fallaron pero el flujo continuo (continue_on_error) |

### Reglas

- Si todos los pasos son exitosos → `0`
- Si algun paso falla y tiene `continue_on_error: false` → `1` (aborta)
- Si algun paso falla pero todos los fallos tenian `continue_on_error: true` → `2`
- Un timeout agotado cuenta como fallo del paso

### Compatibilidad con dag_engine

dag_engine interpreta exit code != 0 como fallo. Para que `2` (partial) no dispare el handler de failure, el DAG que llama a la app puede usar `continue_on.exit_code`:

```yaml
steps:
  - name: run_app
    command: ./mi_app --script flujo.yaml
    continue_on:
      exit_code: [2]   # exit 2 no aborta el DAG (partial es tolerado)
  - name: check_result
    command: test $? -le 1  # falla solo si fue error fatal
    depends: [run_app]
```

O alternativamente, la app puede configurarse con `--strict` para mapear partial → failure (exit 1).

---

## 3. Output Estructurado

Toda app DEBE escribir un **resumen JSON en stdout** al finalizar (despues de su output humano). El formato:

```json
{
  "name": "nombre_del_flujo",
  "status": "success|failure|partial",
  "exit_code": 0,
  "started_at": "2026-04-01T10:00:00Z",
  "ended_at": "2026-04-01T10:00:05Z",
  "duration_ms": 5000,
  "steps_total": 5,
  "steps_ok": 4,
  "steps_failed": 1,
  "steps": [
    {
      "name": "paso_1",
      "action": "navigate",
      "status": "ok",
      "elapsed_ms": 1200,
      "output": "optional output"
    },
    {
      "name": "paso_2",
      "action": "click",
      "status": "error",
      "elapsed_ms": 500,
      "error": "elemento no encontrado: #btn-submit"
    }
  ]
}
```

### Separacion humano / maquina

- **stderr**: logs legibles para humanos (progreso, warnings)
- **stdout**: output JSON estructurado al final (para dag_engine, hooks, pipes)
- Flag `--json` o `--quiet`: suprime output humano, solo JSON en stdout

Esto permite que dag_engine capture el JSON:

```yaml
steps:
  - name: navegacion
    command: ./script_navegador --script busqueda.yaml --json
    output: RESULT
  - name: analizar
    command: echo "$RESULT" | jq '.steps[] | select(.status=="error")'
    depends: [navegacion]
```

---

## 4. Hooks

Los hooks se ejecutan **despues** de todos los pasos, segun el exit code resultante.

### En el YAML del flujo

```yaml
hooks:
  on_success: "notify-send 'Flujo completado'"
  on_failure: "echo 'FALLO: {{.Name}}' >> /tmp/failures.log"
  on_partial: "echo 'PARCIAL: {{.StepsFailed}} pasos fallaron' >> /tmp/warnings.log"
  on_step_error: "echo 'Step {{.StepName}} fallo: {{.Error}}'"  # se ejecuta por cada paso fallido
```

### Variables disponibles en hooks

| Variable | Tipo | Descripcion |
|----------|------|-------------|
| `{{.Name}}` | string | Nombre del flujo |
| `{{.Status}}` | string | success, failure, partial |
| `{{.ExitCode}}` | int | 0, 1, 2 |
| `{{.DurationMs}}` | int | Duracion total en ms |
| `{{.StepsTotal}}` | int | Total de pasos |
| `{{.StepsOk}}` | int | Pasos exitosos |
| `{{.StepsFailed}}` | int | Pasos fallidos |
| `{{.StepName}}` | string | Nombre del paso (solo en on_step_error) |
| `{{.Error}}` | string | Mensaje de error (solo en on_step_error/on_failure) |

### En dag_engine (handlers globales)

Los hooks del YAML son locales al flujo. Para orquestacion, dag_engine maneja sus propios handlers (`init/success/failure/exit`, alias `handler_on`):

```yaml
# apps/dag_engine/dags_migrated/mi_automatizacion.yaml
name: mi_automatizacion
handlers:
  failure:
    - name: alerta
      command: echo "Fallo en mi_automatizacion" >> /var/log/fn_registry/failures.log
  success:
    - name: registrar
      command: echo "OK $(date)" >> /var/log/fn_registry/success.log
steps:
  - name: ejecutar
    function: mi_pipeline_bash_pipelines
    args: ["flujo.yaml"]
```

---

## 5. Integracion con operations.db

Las apps que siguen el bucle reactivo DEBEN registrar la ejecucion en operations.db:

### Mapping del estandar a operations.db

| Concepto estandar | Tabla operations.db | Campo |
|-------------------|---------------------|-------|
| Flujo completo | `executions` | `status`: success/failure/partial |
| Cada paso | `logs` | `level`: info/error, `message`: resultado |
| Exit code | `executions.metrics` | `{"exit_code": N}` |
| Output JSON | `executions.metrics` | `{"steps": [...]}` |
| Duracion | `executions.duration_ms` | milisegundos |
| Pasos in/out | `executions.records_in/out` | total/exitosos |

### Patron de registro (referencia: script_navegador/ops.go)

```
1. initOpsDB()           → abrir/crear operations.db
2. EnsureEntities()      → registrar recursos involucrados
3. EnsureRelations()     → registrar como se conectan
4. [ejecutar flujo]
5. RecordRun()           → insertar execution con metricas
6. LogStep() por paso    → insertar log por cada paso
7. UpdateRelation()      → status final de la relation
```

Este patron ya esta implementado en `script_navegador/ops.go` y sirve como referencia para nuevas apps.

---

## 6. Flujo completo: App → dag_engine → Hooks

```
                  dag_engine (scheduler)
                         │
                    cron / manual / API
                         │
                    ┌────▼────┐
                    │  DAG    │  apps/dag_engine/dags_migrated/mi_dag.yaml
                    │  step   │  command: ./mi_app --script flujo.yaml --json
                    └────┬────┘
                         │
                    ┌────▼────────────────┐
                    │  App (mi_app)       │
                    │                     │
                    │  1. Load YAML       │
                    │  2. Validate steps  │
                    │  3. Init ops.db     │
                    │  4. Execute steps   │
                    │  5. Run hooks       │
                    │  6. Record to ops   │
                    │  7. JSON → stdout   │
                    │  8. Exit code       │
                    └────┬────────────────┘
                         │
              ┌──────────┼──────────┐
              │          │          │
         exit 0     exit 1     exit 2
         success    failure    partial
              │          │          │
              ▼          ▼          ▼
       engine OK   engine FAIL  engine FAIL*
       handler     handler      (configurable)
```

*Con `continue_on: { exit_code: [2] }` en el step, exit 2 no aborta el DAG.

---

## 7. Implementacion: funciones del registry

### Existentes (ya en uso por script_navegador)

Todas las funciones CDP del registry (`cdp_*_go_browser`, `chrome_launch_go_browser`) ya siguen el patron de retornar error. El runner de script_navegador las compone.

### Nuevas (por construir)

| Funcion | Lang | Domain | Descripcion |
|---------|------|--------|-------------|
| `run_steps` | bash | shell | Ejecuta pasos de un YAML generico (action=command/script), reporta JSON y sale con 0/1/2 |
| `report_execution_json` | bash | shell | Genera el JSON de salida estandar dado los resultados de pasos |
| `exit_with_status` | bash | shell | Calcula exit code (0/1/2) a partir de contadores ok/fail/partial |

Estas funciones Bash permiten que cualquier script nuevo siga el estandar sin reimplementar la logica de reporte y exit codes.

---

## 8. Ejemplo completo

### Script YAML (flujo.yaml)

```yaml
name: backup_db
description: "Backup de operations.db y push a remoto"
steps:
  - name: check_db
    action: command
    command: "test -f operations.db"
  - name: backup
    action: command
    command: "cp operations.db backups/operations_$(date +%Y%m%d).db"
    depends: [check_db]
  - name: push
    action: command
    command: "git add backups/ && git commit -m 'backup' && git push"
    depends: [backup]
    continue_on_error: true
hooks:
  on_failure: "echo 'Backup fallo' >> /tmp/backup_errors.log"
```

### Invocacion desde dag_engine

```yaml
name: backup_diario
schedule: "0 2 * * *"
working_dir: /home/lucas/fn_registry/apps/mi_app
steps:
  - name: backup
    function: backup_db_bash_pipelines
    args: ["flujo.yaml", "--json"]
handlers:
  failure:
    - name: alerta
      command: echo "Backup fallo $(date)" >> /var/log/fn_registry/failures.log
```

### Output esperado (stdout)

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

Exit code: `2` (partial — push fallo pero tenia continue_on_error).

---

## Resumen del contrato

| Aspecto | Regla |
|---------|-------|
| Formato de flujo | YAML con `name` + `steps[]` |
| Exit codes | 0=success, 1=failure, 2=partial |
| Output maquina | JSON en stdout (con `--json`) |
| Output humano | stderr (progreso, warnings) |
| Error handling | `continue_on_error` por paso, hooks por resultado |
| Trazabilidad | operations.db (executions + logs) |
| Orquestacion | dag_engine como scheduler (`apps/dag_engine/`), apps/funciones del registry como ejecutores |
| Funciones | Reutilizar del registry, actions por dominio |
