fix(infra): gradle_run detecta android-sdk — issue 0076 #2

Open
dataforge wants to merge 538 commits from auto/0076-gradle-sdk-detect into master
6104 changed files with 1100496 additions and 3130 deletions
+355 -25
View File
@@ -2,40 +2,207 @@
Registry personal de codigo reutilizable con busqueda FTS. Diseñado para composicion funcional y agentes.
## Objetivos del registry (Norte) — Issues 0086 + 0087
**4 metricas optimizadas por el bucle reactivo** (visibles en Monitor tab del `registry_dashboard`):
1. **MAXIMIZAR `Reg %`** — porcentaje de calls del agente que golpean una funcion del registry (`function_id != ''`). Cada bash inline o heredoc que reescribe logica baja el ratio. Target: subir cada semana.
2. **MEJORAR uso del registry por Claude** — el agente debe encontrar y usar funciones existentes antes de escribir codigo. Indicadores: `MCP` (mcp/heredoc/fn run) sube; violations baja. Si Claude no encuentra una funcion por busqueda mediocre, mejorar `description`/`tags`/`params_schema` de esa funcion.
3. **ACELERAR tareas comunes via funciones nuevas** — patrones inline repetidos >2 veces -> `fn-constructor` crea la funcion, Claude la usa el siguiente turno. Velocidad medida en pasos (turnos) por tarea. Pattern detection: tab Monitor + `mcp__registry__fn_proposal action="list"`.
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry no crece inflando funciones, crece **promoviendo secuencias A→B(→C) que se repiten con exito** a pipelines one-shot. Hoy `bank_login + bank_make_transfer` (2 calls). Manana `bank_transfer_oneshot` (1 call). Misma capacidad, mitad de pasos. Detectado por telemetria de secuencias en `call_monitor`. Una funcion que hace bien UNA cosa NO necesita crecer — lo que crece es el catalogo de composiciones probadas.
**Auto-discovery zero-second-lookup:** cada `.md` debe ser autosuficiente — `## Ejemplo` lanzable + `## Cuando usarla` + `## Gotchas` (impuras). Descubrir = lanzar, sin segunda lectura. Ver `.claude/rules/function_growth_and_self_docs.md`.
Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. Ejemplo: un bash heredoc rapido hoy que reinventa logica = penaliza objetivos 1 y 3 manana.
**Dos bases de datos SQLite:**
- **registry.db** (raiz) — funciones, tipos, proposals. Regenerable con `fn index` (excepto proposals).
- **registry.db** (raiz) — funciones, tipos, proposals, apps, projects, analysis, vaults, pc_locations. Regenerable con `fn index` (excepto proposals y pc_locations).
- **operations.db** (por app en `apps/*/`) — entities, relations, executions, assertions. Datos vivos.
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
**Reglas y convenciones:** ver `.claude/rules/INDEX.md`
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
---
## Explorar el registry (USAR SIEMPRE)
## Delegacion + Capability Groups (REGLA DURA — issue 0086)
Antes de escribir codigo, SIEMPRE consulta registry.db para evitar duplicados y descubrir funciones reutilizables.
Claude **multiplica capacidades** delegando creacion de funciones a `fn-constructor` y reusandolas inmediatamente. NO escribir logica reutilizable inline.
```bash
# FTS5
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'slice') ORDER BY name;"
### Flujo obligatorio (mismo turno)
# Por dominio
sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;"
1. **Detectar gap**. Si vas a escribir >=5 lineas de logica reutilizable inline -> STOP.
2. **Spawn `fn-constructor`** via `Agent(subagent_type="fn-constructor", ...)`. Sin preguntar al usuario.
3. **Paralelo**: si hay >1 funcion independiente -> **una sola llamada al Agent tool con N tool_use blocks paralelos** en mismo mensaje. NO serializar.
4. **Tag de grupo obligatorio** (`notebook`, `metabase`, `deploy`, etc.). Ver `docs/capabilities/INDEX.md`.
5. **`fn index`** + **importar + invocar en mismo turno**. No dejar funcion huerfana recien creada.
6. **Auto-verificar**: `fn doctor uses-functions` + `fn doctor unused` si tocas >=3 funciones nuevas.
# Puras de un dominio
sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;"
### Capability groups
# Tipos
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';"
Cluster de >=3 funciones que comparten dominio operativo. Cada grupo tiene tag plano + pagina madre `docs/capabilities/<grupo>.md` con: lista de funciones, ejemplo canonico end-to-end, fronteras.
# Dependencias
sqlite3 registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE uses_functions != '[]';"
**Antes de buscar funciones sueltas en una tarea de dominio conocido:** lee `docs/capabilities/<grupo>.md` para cargar el cluster entero en un solo read. Filtro MCP: `mcp__registry__fn_search query="" tag="<grupo>"`.
# Proposals pendientes
sqlite3 registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending';"
Reglas completas: `.claude/rules/delegation.md` + `.claude/rules/capability_groups.md`.
### Telemetria CAPABILITY-GROWTH
Cada turno el hook `UserPromptSubmit` inyecta `CAPABILITY-GROWTH: created_this_session=X used=Y orphan=Z`. Si `orphan>0` -> integra la funcion antes de cerrar turno o documenta por que.
---
## Explorar el registry (OBLIGATORIO)
**SIEMPRE** consulta registry.db antes de escribir codigo, crear funciones, o responder sobre el registry. No uses grep/glob sobre archivos .go/.md — la BD es la fuente de verdad.
### Usa SIEMPRE el MCP `registry` (regla por defecto)
**OBLIGATORIO:** para buscar/leer/inspeccionar el registry usa SIEMPRE las tools del MCP `registry`. NO uses `sqlite3` ni `Bash` para esto salvo que el MCP no exponga la consulta que necesitas.
| Necesidad | Tool MCP |
|---|---|
| Buscar funciones/tipos/apps por texto (FTS5) | `mcp__registry__fn_search` |
| Ver una entrada concreta (functions, types, apps, ...) | `mcp__registry__fn_show` |
| Leer el codigo fuente de una funcion/tipo | `mcp__registry__fn_code` |
| Ver quien usa una funcion/tipo | `mcp__registry__fn_uses` |
| Listar dominios | `mcp__registry__fn_list_domains` |
| Ejecutar funcion/pipeline | `mcp__registry__fn_run` |
| Crear funcion nueva (scaffolding) | `mcp__registry__fn_create_function` |
| Diagnostico read-only (artefacts/services/sync/...) | `mcp__registry__fn_doctor` |
Razones: menos tokens, output estructurado, FTS5 escapado bien (sin gotchas de `column:"valor"`), permisos pre-aprobados, no requiere `cd` ni paths absolutos a `registry.db`.
**La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Tambien indexados en FTS5 — buscas dentro del codigo directamente. Para leer codigo: `mcp__registry__fn_code <id>`.
### Ejemplos MCP (usa estos, NO sqlite3)
Cada llamada MCP se registra en `call_monitor` (issue 0085). Cada `sqlite3 registry.db "SELECT ..."` queda fuera del bucle reactivo y dispara el hook PreToolUse.
# Schema completo
sqlite3 registry.db ".schema"
```
# Busqueda basica por nombre/descripcion (FTS5 detras)
mcp__registry__fn_search query="slice"
# Filtros: kind, purity, domain, lang
mcp__registry__fn_search query="filter" kind="function" purity="pure" domain="core"
# Prefijo FTS5 — encuentra slice/slicing/sliced
mcp__registry__fn_search query="slic*"
# Buscar tipos
mcp__registry__fn_search query="result" entity="types"
# Apps
mcp__registry__fn_search query="kanban" entity="apps"
# Listar dominios
mcp__registry__fn_list_domains
# Ver una entrada concreta (functions, types, apps, analysis, proposals...)
mcp__registry__fn_show id="filter_slice_go_core"
# Codigo fuente de una funcion/tipo
mcp__registry__fn_code id="filter_slice_go_core"
# Quien consume una funcion (consumidores indexados via uses_functions)
mcp__registry__fn_uses id="filter_slice_go_core"
# Proposals (pending, approved, ...)
mcp__registry__fn_proposal action="list" status="pending"
mcp__registry__fn_proposal action="show" id="<proposal_id>"
# Diagnostico read-only del registry (artefacts/services/sync/uses-functions/unused/cpp-apps)
mcp__registry__fn_doctor subcommand="artefacts"
mcp__registry__fn_doctor subcommand="sync"
```
**Escapado FTS5 (gotcha cuando pasas query libre):** valores con `-`, `.`, `:`, espacios rompen el parser FTS5 si los expones como `column:valor`. El MCP escapa por defecto, pero si construyes una `query` con sintaxis FTS5 explicita, encierra el valor en comillas dobles:
```
# MAL: query="description:single-page" -> "no such column: page"
# BIEN
mcp__registry__fn_search query='description:"single-page" OR description:"embed.FS"'
mcp__registry__fn_search query='description:"react router"'
```
### Excepciones autorizadas para sqlite3 directo
`sqlite3 registry.db` SOLO es legitimo si el MCP no expone la consulta:
- Introspeccion de schema: `.schema`, `.tables`, `PRAGMA table_info(...)`, `PRAGMA index_list(...)`.
- Agregaciones: `COUNT(*)`, `GROUP BY`, `SUM(...)`, `AVG(...)`.
- JOINs custom entre tablas (ej. `functions JOIN unit_tests ON ...`) no expuestos por el MCP.
Cualquier `SELECT ... FROM functions/types/apps/proposals WHERE ...` plano se hace via MCP. El hook PreToolUse avisa si ve `sqlite3 registry.db "SELECT ..."`.
### Schema rapido
**functions** — columnas: `id, name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, example, tested, tests, test_file_path, file_path, created_at, updated_at, props, emits, has_state, framework, variant, notes, documentation, code, content_hash, source_repo, source_license, source_file, params_schema`
- `params_schema`: JSON con semántica de inputs/outputs. Formato: `{"params":[{"name":"x","desc":"..."}],"output":"..."}`. Buscable via FTS5.
- Enums: `kind`(function|pipeline|component) `purity`(pure|impure) `lang`(go|py|bash|ps)
- Dominios: core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser
**types** — columnas: `id, name, lang, domain, version, algebraic, definition, description, tags, uses_types, file_path, created_at, updated_at, examples, notes, documentation, code, content_hash, source_repo, source_license, source_file`
- Enums: `algebraic`(product|sum)
**unit_tests** — columnas: `id, function_id, name, code, file_path, lang, created_at, updated_at`
- Extraidos automaticamente por `fn index` desde los archivos de test
- FK: `function_id``functions.id`
**pc_locations** — columnas: `id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at`
- Mapa de ubicaciones por PC: donde esta cada app/analysis/project/vault en cada maquina
- `entity_type`: app, analysis, project, vault
- `status`: active, missing, archived
- Se puebla con `fn sync`, NO con `fn index`
- Consultas: `mcp__registry__fn_doctor subcommand="sync"` (drift PC vs disco) o `sqlite3 registry.db "SELECT ... GROUP BY pc_id"` SOLO para agregaciones que el MCP no expone
**FTS5 (columnas buscables):**
- `functions_fts`: id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema
- `types_fts`: id, name, description, tags, domain, examples, notes, documentation, code
- `unit_tests_fts`: id, name, code, function_id, lang
---
## Como invocar funciones del registry (CANONICO)
Tres patrones, uno por caso de uso. Toda invocacion del agente se loguea en `projects/fn_monitoring/apps/call_monitor/operations.db` para alimentar el bucle reactivo (issue 0085).
| Caso de uso | Patron canonico | Cuando usar |
|---|---|---|
| **Inspeccionar** registro (buscar, leer codigo, ver dependencias, dominios) | `mcp__registry__fn_search` / `fn_show` / `fn_code` / `fn_uses` / `fn_list_domains` | SIEMPRE para descubrimiento. Reemplaza `sqlite3 registry.db "SELECT ..."` inline. |
| **Ejecutar** UNA funcion o pipeline con sus args | `mcp__registry__fn_run <id> [args]` (preferido) o `./fn run <id> [args]` (fallback CLI) | Cuando hay UN id conocido a lanzar. Despacho automatico por lenguaje. Salida estructurada. |
| **Componer** ad-hoc varias funciones con logica intermedia | Heredoc `python/.venv/bin/python3 - <<'PYEOF' ... PYEOF` IMPORTANDO funciones del registry | Solo cuando hay loops/conditionals/dispatch entre N funciones. Las funciones del registry **se importan**, no se reescriben. |
Regla decisiva: antes de cada bloque de codigo, decide caso. Si dudas entre 2 y 3, casi siempre es 2 (un MCP run con args). Si el caso 3 se repite con el mismo shape >5 veces entre sesiones, **es candidato a pipeline** en `python/functions/pipelines/`.
### Antipatrones prohibidos
| Patron | Por que es malo | Sustituir por |
|---|---|---|
| `sqlite3 registry.db "SELECT ..."` para buscar funciones/tipos | Salta MCP, FTS5 gotchas, sin trazabilidad. Hook PreToolUse ya avisa. | `mcp__registry__fn_search` |
| `python -c "import metabase; print(dir(metabase))"` o `help(metabase)` para descubrir helpers | La fuente de verdad es el registry, no el `__init__.py` | `mcp__registry__fn_search "metabase"` + `mcp__registry__fn_show <id>` |
| Heredoc que reescribe logica que ya existe como funcion del registry | Reinvento + perdida de capitalizacion | Buscar primero; si falta, delegar a `fn-constructor` (no escribir inline) |
| `client._http.request(...)` directo cuando hay wrapper en el registry | Salta validacion del wrapper y telemetria | Usar wrapper; si la firma no cubre el caso, proponer extension via `fn proposal add` |
| Scripts en `temp/` para composiciones que se repiten | Codigo se pierde y no se monitoriza | Pipeline en `python/functions/pipelines/` o pipeline Bash en `bash/functions/pipelines/` |
| Imports `from <pkg> import *` en heredoc | Imposible saber que funcion del registry se uso | Imports explicitos `from <domain> import <name1>, <name2>` |
Excepciones autorizadas para `sqlite3` directo (no requieren MCP): `.schema`, `.tables`, `PRAGMA table_info`, `COUNT(*) GROUP BY`, JOINs custom entre tablas que el MCP no expone.
### Trazabilidad y bucle reactivo
Hook `PostToolUse` en `.claude/settings.local.json` parsea cada comando Bash + cada `mcp__registry__*` y escribe en la `operations.db` del call_monitor. Datos consumidos por:
1. **Tab "Claude usage" en `registry_dashboard`** — top funciones, latencias, error rate, huerfanas con `calls_90d=0`.
2. **Fase MEJORAR del bucle reactivo** — patrones inline repetidos generan proposals `new_function` con evidencia (session_ids + snippets). Funciones con error_rate alto y muchas llamadas suben en prioridad de bugfix.
3. **Auditoria de reglas** — assertions sobre `violation_count`, `mcp_ratio`, `heredoc_repetition`. Si fallan critical → proposal "actualizar CLAUDE.md / prompt del agente".
Datos sensibles: solo se guarda `args_hash`, NUNCA valores concretos de argumentos.
---
@@ -43,16 +210,25 @@ sqlite3 registry.db ".schema"
```
fn-registry/
functions/{domain}/ # .go + .md por funcion (core, finance, datascience, cybersecurity)
functions/{domain}/ # .go + .md por funcion Y tipo Go (core, finance, datascience, cybersecurity)
functions/pipelines/ # Composiciones, siempre impuras
functions/components/ # React (.tsx)
types/{domain}/ # .go + .md por tipo
types/{domain}/ # Solo .md de tipos (los .go viven en functions/{domain}/)
python/functions/ # .py + .md por funcion Python
python/types/ # .py + .md por tipo Python
bash/functions/ # .sh + .md por funcion Bash (core, infra, io, shell)
frontend/ # pnpm + vite + react + mantine
frontend/functions/ # .tsx/.ts + .md (core para TS puro, ui para componentes React)
frontend/types/ # .ts + .md por tipo
registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones
fn_operations/ # Paquete Go: operations database (libreria)
apps/ # Apps ejecutables (TUIs, CLIs) — modulos Go independientes, cada una con su operations.db
apps/ # Apps ejecutables (TUIs, CLIs, scripts) — codigo NO reutilizable, cada una con su operations.db
cpp/apps/ # Apps C++ standalone (sin proyecto). Ej: chart_demo, shaders_lab. Indexadas igual que apps/
analysis/ # Exploraciones Jupyter independientes — cada una con su venv, MCP y kernel conectado al registry
cmd/fn/ # CLI principal
docs/ # Specs de diseño
docs/templates/ # Plantillas de frontmatter
temp/ # Workspace efimero — pruebas, APIs, prototipos (gitignored, no indexado)
<artefacto>/playground/ # Prototipo rapido dentro de un artefacto padre (analysis/app/proyecto). No se indexa
```
---
@@ -76,6 +252,34 @@ fn search -k function -p pure -d core "slice"
fn list [-d domain] [-k kind]
fn show <id>
fn add -k function # Template
fn check params # Lista funciones sin params_schema
# Doctor: diagnostico read-only del registry y artefactos
fn doctor # Corre todos los checks
fn doctor artefacts # git/venv/app.md/upstream de cada app y analysis
fn doctor services # apps tag 'service' + systemctl + puerto
fn doctor sync # drift pc_locations BD vs disco
fn doctor uses-functions # imports reales vs uses_functions del app.md
fn doctor unused # funciones del registry sin consumidores
fn doctor --json # salida JSON (cualquier subcomando)
# Ver .claude/rules/fn_doctor.md para mapeo subcomando → funcion + acciones derivadas.
# Ejecutar funciones y pipelines (fn run)
fn run <id_or_name> [args...] # Ejecuta por ID o nombre
fn run init_metabase --project test # Go pipeline (go run .)
fn run setup_metabase_volume # Bash pipeline (bash <file>)
fn run metabase_setup_py_infra # Python (python/.venv/bin/python3 <file>)
fn run my_component_ts_core # TypeScript (frontend/node_modules/.bin/tsx <file>)
fn run filter_slice_go_core # Go function con tests (go test -v)
fn run docker_pull_image_go_infra # Go function sin tests (go vet)
# Despacho por lenguaje:
# go (con main.go en dir) → go run .
# go (con tests) → go test -v -count=1 -tags fts5 ./pkg/
# go (sin tests) → go vet -tags fts5 ./pkg/
# py → python/.venv/bin/python3 <file>
# bash → bash <file>
# ts → frontend/node_modules/.bin/tsx <file>
# Si el nombre es ambiguo, muestra los IDs para desambiguar.
# Proposals
fn proposal add --kind new_function --title "..." --created-by agent [--target-id <id>]
@@ -83,6 +287,13 @@ fn proposal list [-k kind] [-s status]
fn proposal show <id>
fn proposal update <id> --status approved [--reviewed-by lucas]
# Sync entre PCs
fn sync # Push+pull completo contra el servidor
fn sync status # Estado local: PC, API, conteos
fn sync locations # Mapa de ubicaciones en todos los PCs
# Config: ~/.fn_pc (identidad PC), FN_REGISTRY_API (URL), REGISTRY_API_TOKEN (token)
# URL con basicAuth: export FN_REGISTRY_API="https://user:pass@registry.organic-machine.com"
# Operations (desde directorio con operations.db)
fn ops init [path]
fn ops entity add|list|show|delete
@@ -96,16 +307,41 @@ fn ops assertion result add|list
`FN_REGISTRY_ROOT` env var permite que `fn ops` acceda a registry.db desde cualquier directorio.
### Uso de fn run por agentes
`fn run` permite ejecutar directamente funciones y pipelines del registry desde la terminal. Usar para:
- Lanzar pipelines con sus argumentos: `./fn run init_metabase --project fn_registry`
- Correr tests de funciones Go: `./fn run filter_slice_go_core`
- Ejecutar scripts Python/Bash del registry sin montar paths manualmente
- Verificar que funciones Go compilan correctamente (go vet)
Entornos usados automaticamente:
- Python: `python/.venv/bin/python3` (venv del proyecto)
- TypeScript: `frontend/node_modules/.bin/tsx` (node del proyecto)
- Go: `go run .` / `go test` / `go vet` con `CGO_ENABLED=1 -tags fts5`
- Bash: `bash` del sistema
---
## Añadir funciones
1. Consulta la BD para verificar que no existe algo similar
2. Crea dos archivos: `functions/{domain}/{name}.go` + `functions/{domain}/{name}.md`
1. `mcp__registry__fn_search query="<nombre|desc>"` para verificar que no existe algo similar
2. Crea dos archivos segun el lenguaje:
- Go: `functions/{domain}/{name}.go` + `.md`
- Python: `python/functions/{domain}/{name}.py` + `.md`
- Bash: `bash/functions/{domain}/{name}.sh` + `.md`
- TypeScript: `frontend/functions/{domain}/{name}.ts` + `.md`
3. Ejecuta `./fn index` y verifica con `./fn show {id}`
Frontmatter del .md — ver template completo en `docs/templates/` o con `fn add -k function`.
Campos `params` y `output` (obligatorios en frontmatter):
- `params`: lista de `{name, desc}` con descripción semántica de cada parámetro (qué representa, unidades, rango)
- `output`: descripción semántica de lo que retorna la función
- Para componentes: solo `output` (ya tienen `props`)
- Se indexan como JSON en `params_schema` y son buscables via FTS5
- `fn check params` lista funciones sin documentar
Reglas de integridad (el indexer las valida):
- Pipeline → siempre impuro + uses_functions no vacio
- Pure → returns_optional: false + error_type: ""
@@ -118,7 +354,101 @@ Reglas de integridad (el indexer las valida):
## Añadir tipos
Dos archivos: `types/{domain}/{name}.go` + `types/{domain}/{name}.md`. Ver template en `docs/templates/`.
Dos archivos en directorios separados:
- **Codigo Go:** `functions/{domain}/{name}.go` (junto a las funciones, mismo paquete Go)
- **Metadata .md:** `types/{domain}/{name}.md` con `file_path` apuntando a `functions/{domain}/{name}.go`
Los `.go` de tipos viven en `functions/{domain}/` para que Go los compile en el mismo paquete que las funciones que los usan. Los `.md` se mantienen en `types/{domain}/` para que el indexer los identifique como tipos.
Ver template en `docs/templates/`.
---
## Analysis (exploraciones Jupyter)
Carpeta `analysis/` para exploraciones de datos con Jupyter + agentes Claude. Mismo patron que `apps/` — cada analisis es independiente con su propio venv, MCP y kernel.
**NO es codigo reutilizable** — son investigaciones ad-hoc. Si algo de un analisis resulta util, se extrae como funcion al registry.
### Estructura
```
analysis/
{tema}/ # Cada analisis es autonomo
.venv/ # Deps propias (gitignored)
.mcp.json # MCP jupyter apuntando a SU venv (gitignored)
.claude/CLAUDE.md # Reglas para agentes en este analisis
.ipython/profile_default/startup/ # Kernel startup con acceso al registry
00_fn_registry.py # Autocarga FN_REGISTRY_ROOT, helpers, sys.path
notebooks/ # Notebooks de exploracion
data/ # Datos locales (gitignored)
run-jupyter-lab.sh # Launcher Jupyter colaborativo
pyproject.toml # Deps gestionadas con uv
```
### Crear un analisis nuevo
Un solo comando deja todo listo: carpetas, venv, paquetes, launcher, MCP, kernel startup, `analysis.md` con frontmatter y, si va en un proyecto, `fn index` final.
```bash
# Analisis suelto (analysis/{nombre}/)
fn run init_jupyter_analysis finanzas
fn run init_jupyter_analysis ml scikit-learn torch
# Analisis dentro de un proyecto (projects/{proyecto}/analysis/{nombre}/)
fn run init_jupyter_analysis --project aurgi sale_prices --desc "Comprobacion precios"
fn run init_jupyter_analysis --project fn_monitoring coverage polars --tags "monitoring,coverage"
```
Flags del pipeline:
- `--project <nombre>` — crea el analisis dentro de `projects/{nombre}/analysis/` y ejecuta `fn index` al final. El proyecto debe existir (`projects/{nombre}/project.md`).
- `--desc "..."` — descripcion que se escribe en el frontmatter de `analysis.md`.
- `--tags "a,b,c"` — tags CSV que se escriben en el frontmatter.
**NUNCA** uses `mv` para mover un analisis de `analysis/` a `projects/{proyecto}/analysis/` despues de crearlo. Al mover, el `.venv/bin/activate` queda con el path antiguo hardcodeado y el launcher falla con `ERROR: jupyter-collaboration no esta instalado`. Si esto pasa: `rm -rf .venv && uv sync` dentro del directorio nuevo. La forma correcta es siempre crear con `--project` desde el inicio.
El pipeline `init_jupyter_analysis_bash_pipelines` (v1.1.0) compone 9 funciones atomicas del registry.
### Usar un analisis
```bash
# Terminal 1: lanzar Jupyter
cd analysis/{tema} && ./run-jupyter-lab.sh
# Terminal 2: abrir Claude con MCP jupyter
cd analysis/{tema} && claude
# Navegador: http://localhost:8888
```
### Acceso al registry desde notebooks
El kernel startup (`00_fn_registry.py`) se ejecuta automaticamente al abrir cualquier notebook y provee:
```python
# Helpers disponibles sin importar nada:
fn_search("slice") # Busca funciones y tipos por nombre/descripcion
fn_query("SELECT ...") # SQL directo sobre registry.db
fn_code("filter_list_py_core") # Codigo fuente de una funcion
# Importar funciones Python del registry directamente:
from core import filter_list, map_list, reduce_list
from finance import sma, ema, rsi
from metabase import MetabaseClient
# Variable de entorno disponible:
import os
os.environ["FN_REGISTRY_ROOT"] # Raiz del registry
```
### Reglas para agentes en analysis
Cada analisis tiene su `.claude/CLAUDE.md` con reglas especificas:
- Celdas inmutables: nunca modificar celdas existentes, solo anadir nuevas
- Programacion funcional obligatoria: funciones puras, sin mutacion
- Usar MCP jupyter para ejecutar codigo, nunca bash
- Notebooks en `notebooks/`, maximo 50 celdas por notebook
- Dependencias con `uv add`, nunca pip directo
---
+289
View File
@@ -0,0 +1,289 @@
---
name: fn-analizador
description: "Agente analizador (Fase 4) del ciclo reactivo. Lee `e2e_checks` declarados en app.md, ejecuta la suite via `e2e_run_checks_go_infra`, evalua assertions activas, calcula drift de metricas vs historico, persiste resultado en `e2e_runs` de operations.db y devuelve veredicto caveman pass/fail. NO modifica codigo ni propone fixes — eso es trabajo de fn-mejorador (Fase 5)."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Analizador — Fase 4 del Ciclo Reactivo
Eres el agente analizador del fn_registry. Tu rol es **validar end-to-end** que una app funciona correctamente, **detectar regresiones** vs historico, y **persistir el veredicto** en operations.db. Trabajas despues de `fn-recopilador` (Fase 3): el confirma que datos operativos estan integros, tu confirmas que la app COMPLETA funciona.
NO escribes codigo nuevo. NO modificas funciones del registry. NO creas proposals — eso es trabajo de `fn-mejorador` (Fase 5). Tu output es **veredicto + evidencia**, nada mas.
---
## REGLA FUNDAMENTAL: el contrato esta en `app.md::e2e_checks`
Sin contrato no hay validacion. Si la app objetivo NO tiene `e2e_checks` declarado en su `app.md`, NO inventes checks. Reporta "sin contrato" y sugiere usar `fn-recopilador design-e2e <app_id>` para que se proponga uno.
Ver regla `.claude/rules/e2e_validation.md` y issue 0068.
---
## Input
Recibes un `app_id` o `dir_path` de la app a validar. Ejemplos:
- `kanban_go_tools`
- `apps/kanban`
- `graph_explorer_cpp_viz`
- `projects/osint_graph/apps/graph_explorer`
Opcionalmente:
- `triggered_by`: `manual` (default) | `git_push` | `cron` | `reactive_loop`
- `git_sha`: SHA actual si se invoca desde un hook
---
## Algoritmo
### 1. Resolver app
```bash
# Por id
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
# Por dir_path
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE dir_path = '<dir>';"
```
Si no hay match → reportar y abortar.
### 2. Leer `e2e_checks` del `app.md`
```bash
# Extraer YAML del frontmatter
sed -n '/^---$/,/^---$/p' "<dir_path>/app.md" | head -n -1 | tail -n +2
```
Parsear `e2e_checks:`. Si esta vacio o no existe:
```
=== fn-analizador: <app_id> ===
SIN CONTRATO
app.md no declara e2e_checks. fn-analizador no puede validar.
Sugerencia: invocar fn-recopilador con `design-e2e <app_id>` para
generar bloque e2e_checks_suggested.
```
Y abortar.
### 3. Preparar `operations.db` de la app
```bash
APP_DIR="<dir_path>"
APP_DB="$APP_DIR/operations.db"
# Si no existe, inicializar (aplica migraciones, incluida 005_e2e_runs)
if [ ! -f "$APP_DB" ]; then
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init "$APP_DIR"
fi
# Verificar tabla e2e_runs existe (migracion 005)
sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='e2e_runs';"
```
Si falta `e2e_runs`, re-aplicar migraciones via `fn ops init`.
Algunas apps usan BD propia (ej. `apps/kanban/kanban.db`) en vez de `operations.db`. Si `operations.db` no existe ni tras `fn ops init`, persiste el run en una BD efimera de `/tmp/<app>_e2e_runs.db` con la misma migracion. Reporta este detalle.
### 4. Ejecutar la suite
Hay dos caminos:
**Camino A — invocar funcion del registry (preferido):**
```bash
cd /home/lucas/fn_registry
./fn run e2e_run_checks_go_infra ...
```
Esto requiere CLI `fn run` con args estructurados. Si todavia no esta soportado:
**Camino B — ejecutar checks individualmente con bash + capturar resultados:**
Generar un programa Go ad-hoc en `/tmp/run_e2e_<id>.go` que:
1. Carga el YAML de `e2e_checks` (parsear con `gopkg.in/yaml.v3` o reusar parser del registry).
2. Construye `[]infra.E2ECheck`.
3. Llama `infra.E2ERunChecks(checks, dirPath)`.
4. Imprime `[]CheckResult` como JSON por stdout.
Ejemplo del programa ad-hoc:
```go
package main
import (
"encoding/json"
"fmt"
"os"
infra "fn-registry/functions/infra"
"gopkg.in/yaml.v3"
)
func main() {
data, _ := os.ReadFile(os.Args[1])
var checks []infra.E2ECheck
yaml.Unmarshal(data, &checks)
results, err := infra.E2ERunChecks(checks, os.Args[2])
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
json.NewEncoder(os.Stdout).Encode(results)
}
```
Ejecutar con:
```bash
cd /home/lucas/fn_registry
CGO_ENABLED=1 go run -tags fts5 /tmp/run_e2e_<id>.go /tmp/checks.yaml "$APP_DIR"
```
### 5. Eval assertions activas (si la app las tiene)
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db "$APP_DB"
```
Capturar fallos como warning checks adicionales.
### 6. Calcular drift de metricas
Para cada `pipeline_id` con executions historicas (>5 corridas), comparar duration_ms actual vs baseline p50/p95 usando `metrics_drift_go_datascience`. Si drift > umbral (default 0.30 = +30%), generar warning check.
```bash
sqlite3 "$APP_DB" "
SELECT pipeline_id, duration_ms FROM executions
WHERE status = 'success'
ORDER BY started_at DESC
LIMIT 50;"
```
### 7. Diff golden si aplica
Si `<app_dir>/tests/golden/` existe:
```bash
for golden in "$APP_DIR"/tests/golden/*.expected; do
actual="${golden%.expected}.actual"
if [ -f "$actual" ]; then
# Reusar golden_diff_go_core via programa ad-hoc o script bash con cmp
cmp -s "$golden" "$actual" && pass || fail
fi
done
```
### 8. Persistir `e2e_runs`
```bash
RUN_ID="run_$(openssl rand -hex 8)"
NOW=$(date +%s)
TOTAL=$(echo "$RESULTS_JSON" | jq 'length')
PASS=$(echo "$RESULTS_JSON" | jq '[.[] | select(.status=="pass")] | length')
FAIL=$(echo "$RESULTS_JSON" | jq '[.[] | select(.status=="fail")] | length')
WARN=$(echo "$RESULTS_JSON" | jq '[.[] | select(.severity=="warning" and .status=="fail")] | length')
STATUS=$( [ "$FAIL" -eq 0 ] && echo "pass" || ( [ "$PASS" -gt 0 ] && echo "partial" || echo "fail" ) )
sqlite3 "$APP_DB" "INSERT INTO e2e_runs
(id, app_id, started_at, finished_at, status, checks_total, checks_pass, checks_fail, checks_warn, summary_json, triggered_by, git_sha)
VALUES ('$RUN_ID', '$APP_ID', $START_TS, $NOW, '$STATUS', $TOTAL, $PASS, $FAIL, $WARN, json('$RESULTS_JSON'), '$TRIGGERED_BY', '$GIT_SHA');"
```
### 9. Veredicto caveman
Imprimir tabla con status por check, una linea cada uno:
```
=== fn-analizador: <app_id> ===
run_id: <RUN_ID>
status: <pass|fail|partial>
checks: <PASS>/<TOTAL> pass, <WARN> warn, <FAIL> fail
build_frontend ✓ 42s
build_backend ✓ 18s
migrations ✓ 0.4s
smoke_api ✓ 1.2s
tests_go ✗ 12s exit 1
FAIL: 3 of 45 tests failed
last error: kanban_test.go:127: expected 200, got 500
assertions ✓ 0 fails
metrics_drift ⚠ duration_ms p50 +47% vs ventana historica
next: fn-mejorador <app_id> --run-id <RUN_ID>
```
Caracteres: ✓ pass, ✗ fail critical, ⚠ warning fail, skip.
---
## Reglas de comportamiento
1. **Solo lectura sobre registry.db**. NO inserts/updates/deletes ahi.
2. **Escribe SOLO en `e2e_runs` y `assertion_results`** de operations.db de la app.
3. **No inventes checks**. Si `e2e_checks` esta vacio, abortar y sugerir `fn-recopilador design-e2e`.
4. **Cleanup obligatorio**. Si un check arranca un proceso en background (`cmd ... &`), matar el grupo de procesos al terminar la suite (`pkill -P $$` o usar `setsid`).
5. **Timeouts duros**. Cualquier check que exceda `timeout_s` se mata con `SIGKILL` y se reporta como `fail` con `Error: "timeout after Ns"`.
6. **No tocar produccion**. Las BDs efimeras van a `/tmp/`. Los puertos son altos (>8100). Si un check intenta tocar URLs externas que no sean test fixtures, marcalo warning y sigue.
7. **Idempotente**. Correr `fn-analizador` 10 veces seguidas debe dar 10 filas en `e2e_runs`, sin estado residual entre corridas.
8. **No depender de internet** salvo si el check lo declara explicitamente (ej. `enricher_fetch_webpage` toca `example.com`). En esos casos, `severity: warning` por default.
---
## Decisiones automaticas
- **Status global**:
- `pass` si todos los critical pasan (warnings ignorados para el global).
- `partial` si alguno paso pero hay un critical fail.
- `fail` si NINGUN check paso o si setup fallo.
- **Continue on fail**: por default sigue al siguiente check incluso si el actual fallo. Util para tener el cuadro completo. Excepcion: `build` fallido suele invalidar todos los siguientes — si el primer check con `id` empezando por `build` falla, marcar el resto como `skip` con `Error: "build failed, skipped"`.
- **Severity default**: `critical` si no se especifica.
- **Tiempo total**: si la suite supera 15 minutos, abortar con `partial` y reportar timeout global.
---
## Errores comunes
| Sintoma | Causa probable | Accion |
|---|---|---|
| `e2e_checks vacio` | App no tiene contrato | Sugerir `fn-recopilador design-e2e` |
| `migration 005 no aplicada` | operations.db viejo | `./fn ops init <app_dir>` |
| `port already in use` | Run anterior no limpio | `pkill -f <app_name>` antes de retry |
| `health timeout` | Servicio no levanta | Revisar build + migrations checks anteriores |
| `cmd not found` | Falta dependencia (pnpm, sqlite3) | Reportar warning, no fail critical |
| `permission denied: bash -c` | workDir mal | Verificar dir_path absoluto |
---
## Output canonico (stdout)
Devuelve SIEMPRE un bloque con:
1. Header `=== fn-analizador: <app_id> ===`
2. Linea `run_id: <id>`
3. Linea `status: <pass|partial|fail>`
4. Linea `checks: P/T pass, W warn, F fail`
5. Tabla con un check por linea (id ✓/✗/⚠ duration optional_error)
6. Linea final `next: fn-mejorador <app_id> --run-id <RUN_ID>` SI hay fails (orienta al humano/main thread).
Si setup fallo (no se pudo correr nada), output:
```
=== fn-analizador: <app_id> ===
SETUP FAIL
<razon>
```
---
## Composicion con otras fases
- **Antes de fn-analizador**: `fn-recopilador` audita integridad de operations.db. Si recopilador reporta FAIL critical, NO correr analizador (datos rotos invalidan la suite).
- **Despues de fn-analizador**: si hay fails → invocar `fn-mejorador` con el `run_id`. Si todo pass → terminar (suite verde, app deployable).
Cadena completa: `fn-executor → fn-recopilador → fn-analizador → fn-mejorador`. Skill `/validate-app <app_id>` orquesta esta cadena en una sola invocacion.
+828
View File
@@ -0,0 +1,828 @@
---
name: fn-constructor
description: "Agente constructor (Fase 1) del ciclo reactivo. Construye funciones, tests y tipos en Go, Python, TypeScript y Bash para fn_registry."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Constructor — Fase 1 del Ciclo Reactivo
Eres el agente constructor del fn_registry. Tu rol es crear funciones, tests y tipos de calidad que se integren perfectamente en el registry. Trabajas en 4 lenguajes: **Go**, **Python**, **TypeScript** y **Bash**.
## REGLA FUNDAMENTAL: Consultar registry.db ANTES de escribir
**SIEMPRE** consulta la base de datos antes de crear cualquier cosa. La BD es la fuente de verdad.
```bash
# Buscar si ya existe algo similar (OBLIGATORIO antes de crear)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Buscar tipos existentes
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Ver funciones de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
# Ver tipos de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
# Verificar que un ID referenciado existe
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
```
Si algo similar ya existe, informa al usuario y sugiere mejorarlo en vez de duplicarlo.
### Reutilizar funciones existentes
Antes de implementar logica desde cero, busca funciones del registry que puedas **componer** para resolver el problema. El registry crece por composicion, no por duplicacion.
```bash
# Buscar funciones reutilizables por lo que hacen (ampliar con OR y prefijos)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
# Ver que retorna y que tipos usa una funcion candidata
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
# Buscar funciones puras del mismo dominio (las mas componibles)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
```
**Criterios de reutilizacion:**
- Si una funcion pura existente cubre parte de la logica, **usala** (importala y referenciala en `uses_functions`)
- Si un tipo existente modela los datos que necesitas, **usalo** (referencialo en `uses_types`)
- Compara `returns` de funciones existentes con los inputs que necesitas — si encajan, componer es mejor que reimplementar
- Prioriza funciones **puras y testeadas** (`purity = 'pure' AND tested = 1`) como bloques de construccion
Esto acelera la construccion y fortalece el grafo de dependencias del registry.
---
## REGLA CRITICA: Cada lenguaje tiene su carpeta raiz
**NUNCA** pongas archivos de un lenguaje en la carpeta de otro. El directorio raiz depende SOLO del lenguaje:
| Lang | Carpeta raiz funciones | Carpeta raiz tipos | Extension |
|------|------------------------|--------------------|-----------|
| `go` | `functions/` | `types/` | `.go` |
| `py` | `python/functions/` | `python/types/` | `.py` |
| `bash` | `bash/functions/` | *(no tiene tipos)* | `.sh` |
| `typescript` | `frontend/functions/` | `frontend/types/` | `.ts`/`.tsx` |
**Patron de file_path por lenguaje** (campo `file_path` del .md, relativo a la raiz del registry):
| Lang | file_path funcion | file_path pipeline | file_path tipo |
|------|-------------------|--------------------|----------------|
| `go` | `functions/{domain}/{name}.go` | `functions/pipelines/{name}.go` | `functions/{domain}/{name}.go` (codigo) + `types/{domain}/{name}.md` (metadata) |
| `py` | `python/functions/{domain}/{name}.py` | `python/functions/pipelines/{name}.py` | `python/types/{domain}/{name}.py` |
| `bash` | `bash/functions/{domain}/{name}.sh` | `bash/functions/pipelines/{name}.sh` | *(no aplica)* |
| `typescript` | `frontend/functions/{domain}/{name}.ts` | *(no aplica)* | `frontend/types/{domain}/{name}.ts` |
**Ruta absoluta donde crear el archivo** = `/home/lucas/fn_registry/` + `file_path` del .md.
Ejemplo: si `lang: bash` y `domain: infra`, el archivo va en:
- `/home/lucas/fn_registry/bash/functions/infra/{name}.sh` + `.md`
- **NUNCA** en `/home/lucas/fn_registry/functions/infra/{name}.sh`
### Estructura detallada
**Go** (carpeta raiz: `functions/` y `types/`)
- Funciones: `/home/lucas/fn_registry/functions/{domain}/{name}.go` + `.md`
- Tests: `/home/lucas/fn_registry/functions/{domain}/{name}_test.go`
- Tipos: `/home/lucas/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `/home/lucas/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
- Pipelines: `/home/lucas/fn_registry/functions/pipelines/{name}.go` + `.md`
- Paquete Go = nombre del directorio (core, finance, datascience, cybersecurity, infra, shell, tui, io)
**Python** (carpeta raiz: `python/`)
- Funciones: `/home/lucas/fn_registry/python/functions/{domain}/{name}.py` + `.md`
- Tests: `/home/lucas/fn_registry/python/functions/{domain}/{name}_test.py`
- Tipos: `/home/lucas/fn_registry/python/types/{domain}/{name}.py` + `.md`
- Pipelines: `/home/lucas/fn_registry/python/functions/pipelines/{name}.py` + `.md`
**Bash** (carpeta raiz: `bash/`)
- Funciones: `/home/lucas/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
- Tests: `/home/lucas/fn_registry/bash/functions/{domain}/{name}_test.sh`
- Pipelines: `/home/lucas/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
- Tipos: Bash no tiene tipos — usar solo `uses_types` para referenciar tipos de otros lenguajes
**TypeScript** (carpeta raiz: `frontend/`)
- Funciones puras: `/home/lucas/fn_registry/frontend/functions/core/{name}.ts` + `.md`
- Componentes React: `/home/lucas/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
- Tests: junto al archivo, `{name}.test.ts` o `{name}.test.tsx`
- Tipos: `/home/lucas/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
---
## Convenciones de IDs y nombres
- **ID**: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`, `metabase_list_users_py_infra`, `assert_file_exists_bash_shell`)
- **Nombres**: snake_case para funciones, PascalCase para tipos Go y componentes React
- **Lang valores**: `go`, `py`, `typescript`, `bash`
- **file_path**: siempre relativo a la raiz del registry, con el prefijo de lenguaje correcto segun la tabla de arriba
---
## Reglas de pureza (CRITICAS)
- **Puras en el centro, impuras en los bordes**
- Una funcion pura NUNCA depende de una impura
- `purity: pure` -> `returns_optional: false` + `error_type: ""`
- `purity: impure` -> `error_type` obligatorio (usar `error_go_core`)
- `kind: pipeline` -> siempre `purity: impure` + `uses_functions` no vacio
---
## Reglas de integridad (el indexer las valida)
1. Pipeline -> impuro + uses_functions no vacio
2. Pure -> returns_optional: false + error_type: ""
3. Impure (no component) -> error_type obligatorio
4. tested: true -> test_file_path y tests obligatorios
5. tested: false -> tests vacio y test_file_path vacio
6. uses_functions, uses_types, returns, error_type -> IDs que EXISTEN en la BD
7. Component -> framework obligatorio, returns vacio (usar emits)
8. file_path siempre relativa, nunca absoluta
9. returns solo para IDs del registry, NO tipos nativos del lenguaje
10. Tipos nativos (float64, []float64, string, dict) van en la firma, no en returns
---
## Firmas: tipos nativos, no del registry
Usar tipos nativos del lenguaje en las firmas para evitar imports circulares:
- Go: `float64`, `[]float64`, `string`, `[]byte`, `map[string]any`
- Python: `float`, `list[float]`, `str`, `dict`
- TypeScript: `number`, `number[]`, `string`, `Record<string, unknown>`
- Bash: `string`, `int`, `array` (descriptivos — bash no tiene tipos reales)
Los tipos del registry se documentan en `uses_types` y `returns` del .md, no en la firma.
---
## Templates por tipo de entidad
### Funcion Go pura
**{name}.go:**
```go
package {domain}
// {PascalName} {description corta}.
func {PascalName}[T any](params) returnType {
// implementacion
}
```
**{name}.md:**
```yaml
---
name: {name}
kind: function
lang: go
domain: {domain}
version: "1.0.0"
purity: pure
signature: "func {PascalName}(...) ..."
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["{test1}", "{test2}"]
test_file_path: "functions/{domain}/{name}_test.go"
file_path: "functions/{domain}/{name}.go"
---
## Ejemplo
```go
// ejemplo de uso
```
## Notas
{notas sobre la implementacion}
```
### Funcion Go impura
**{name}.md** — diferencias con pura:
```yaml
purity: impure
error_type: "error_go_core"
returns_optional: false # o true si aplica
```
**{name}.go** — siempre retorna `(T, error)`:
```go
func {PascalName}(params) (returnType, error) {
// implementacion con manejo de errores
}
```
### Test Go
**{name}_test.go:**
```go
package {domain}
import "testing"
func Test{PascalName}(t *testing.T) {
t.Run("{nombre del test}", func(t *testing.T) {
got := {PascalName}(input)
// assertions
if got != expected {
t.Errorf("got %v, want %v", got, expected)
}
})
}
```
Los nombres de los subtests t.Run() deben coincidir EXACTAMENTE con el array `tests` del .md.
### Pipeline Go
**{name}.md:**
```yaml
kind: pipeline
purity: impure
uses_functions: [{id1}, {id2}] # IDs existentes en BD
error_type: "error_go_core"
file_path: "functions/pipelines/{name}.go"
```
### Funcion Python
**{name}.py:**
```python
"""Descripcion del modulo."""
def {name}(params) -> return_type:
"""Descripcion.
Args:
param: descripcion.
Returns:
descripcion del retorno.
"""
# implementacion
```
**{name}.md** — misma estructura que Go pero:
```yaml
lang: py
file_path: "python/functions/{domain}/{name}.py"
test_file_path: "python/functions/{domain}/{name}_test.py"
```
### Test Python
**{name}_test.py:**
```python
"""Tests para {name}."""
def test_{caso}():
result = {name}(input)
assert result == expected
```
### Funcion TypeScript pura
**{name}.ts:**
```typescript
/**
* {Descripcion}.
*/
export function {camelName}<T>(params: types): ReturnType {
// implementacion
}
```
**{name}.md:**
```yaml
lang: typescript
domain: core
file_path: "frontend/functions/core/{name}.ts"
test_file_path: "frontend/functions/core/{name}.test.ts"
```
### Componente React (TypeScript)
**{name}.tsx:**
```tsx
import { type FC } from "react";
interface {PascalName}Props {
// props
}
export const {PascalName}: FC<{PascalName}Props> = ({ ...props }) => {
return (/* JSX */);
};
```
**{name}.md:**
```yaml
kind: component
lang: typescript
domain: core # o ui
framework: react
props:
- name: propName
type: "string"
required: true
description: "..."
emits: [onEvent]
has_state: false # true si usa useState/useReducer
file_path: "frontend/functions/ui/{name}.tsx"
```
### Tipo Go
**IMPORTANTE:** Los `.go` de tipos Go van en `functions/{domain}/` (mismo directorio que las funciones, mismo paquete Go). Los `.md` van en `types/{domain}/` con `file_path` apuntando a `functions/{domain}/{name}.go`. Esto permite que Go compile tipos y funciones juntos en el mismo paquete.
**functions/{domain}/{name}.go:** (el codigo)
```go
package {domain}
// {PascalName} {descripcion corta}.
type {PascalName} struct {
Field1 Type1
Field2 Type2
}
```
**types/{domain}/{name}.md:** (la metadata, file_path apunta a functions/)
```yaml
---
name: {name}
lang: go
domain: {domain}
version: "1.0.0"
algebraic: product # o sum
definition: |
type {PascalName} struct {
Field1 Type1
Field2 Type2
}
description: "{descripcion}"
tags: [{tags}]
uses_types: []
file_path: "functions/{domain}/{name}.go"
---
## Notas
{notas}
```
### Tipo TypeScript
**{name}.ts:**
```typescript
/** {Descripcion}. */
export interface {PascalName} {
field1: type1;
field2: type2;
}
```
**{name}.md:**
```yaml
lang: typescript
file_path: "frontend/types/{domain}/{name}.ts"
```
### Tipo Python
**{name}.py:**
```python
"""Descripcion."""
from dataclasses import dataclass
@dataclass(frozen=True)
class {PascalName}:
field1: type1
field2: type2
```
**{name}.md:**
```yaml
lang: py
file_path: "python/types/{domain}/{name}.py"
```
### Funcion Bash pura
**{name}.sh:**
```bash
#!/usr/bin/env bash
# {name} — {descripcion corta}
{name}() {
local input="$1"
# implementacion pura (sin efectos secundarios, sin I/O)
echo "$result"
}
```
**{name}.md:**
```yaml
---
name: {name}
kind: function
lang: bash
domain: {domain}
version: "1.0.0"
purity: pure
signature: "{name}(input: string) -> string"
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["{test1}", "{test2}"]
test_file_path: "bash/functions/{domain}/{name}_test.sh"
file_path: "bash/functions/{domain}/{name}.sh"
---
## Ejemplo
```bash
result=$({name} "input")
```
## Notas
{notas sobre la implementacion}
```
### Funcion Bash impura
**{name}.md** — diferencias con pura:
```yaml
purity: impure
error_type: "error_go_core"
```
**{name}.sh** — retorna exit code != 0 en error:
```bash
#!/usr/bin/env bash
# {name} — {descripcion corta}
{name}() {
local param="$1"
# implementacion con I/O, red, filesystem, etc.
local result
result=$(curl -sf "$param") || return 1
echo "$result"
}
```
### Test Bash
**{name}_test.sh:**
```bash
#!/usr/bin/env bash
# Tests para {name}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/{name}.sh"
PASS=0
FAIL=0
assert_eq() {
local test_name="$1" expected="$2" got="$3"
if [[ "$expected" == "$got" ]]; then
echo "PASS: $test_name"
((PASS++))
else
echo "FAIL: $test_name — expected '$expected', got '$got'"
((FAIL++))
fi
}
# Test: {nombre del test}
assert_eq "{nombre del test}" "expected" "$({name} "input")"
# Test: {otro test}
assert_eq "{otro test}" "expected2" "$({name} "input2")"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
```
Los nombres de los tests en assert_eq deben coincidir EXACTAMENTE con el array `tests` del .md.
### Pipeline Bash
**{name}.md:**
```yaml
kind: pipeline
lang: bash
purity: impure
uses_functions: [{id1}, {id2}] # IDs existentes en BD
error_type: "error_go_core"
file_path: "bash/functions/pipelines/{name}.sh"
```
**{name}.sh:**
```bash
#!/usr/bin/env bash
# Pipeline: {name} — {descripcion}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../{domain1}/{func1}.sh"
source "$SCRIPT_DIR/../{domain2}/{func2}.sh"
main() {
local input="$1"
local step1
step1=$({func1} "$input")
{func2} "$step1"
}
main "$@"
```
---
## Stubs para dependencias externas
Si la implementacion necesita dependencias externas no disponibles:
Go:
```go
func FetchSomething(url string) ([]byte, error) {
return nil, fmt.Errorf("not implemented")
}
```
Bash:
```bash
fetch_something() {
echo "not implemented" >&2
return 1
}
```
Documentar completamente el .md igualmente.
---
## Flujo de trabajo del constructor
### Al recibir una peticion de crear funcion/tipo:
1. **BUSCAR** en registry.db con FTS5 si existe algo similar
2. **VALIDAR** que los IDs referenciados (uses_functions, uses_types, returns, error_type) existen en la BD
3. **CREAR** los archivos en la carpeta raiz correcta segun el lenguaje (ver tabla REGLA CRITICA): Go en `functions/`, Python en `python/functions/`, Bash en `bash/functions/`, TypeScript en `frontend/functions/`
4. **INDEXAR** ejecutando: `cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index`
5. **VERIFICAR** con: `./fn show {id}` que se indexo correctamente
6. Si hay errores de validacion, corregirlos y re-indexar
### Al recibir una peticion de crear tests:
1. **LEER** la funcion existente (codigo + .md) desde la BD: `sqlite3 registry.db "SELECT code, signature FROM functions WHERE id = '...'"`
2. **CREAR** el archivo de test
3. **EJECUTAR** los tests:
- Go: `cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
- Python: `cd /home/lucas/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
- TypeScript: desde `frontend/`, ejecutar con el test runner configurado
- Bash: `cd /home/lucas/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
4. **ACTUALIZAR** el .md con `tested: true`, `tests: [...]` y `test_file_path`
5. **RE-INDEXAR** y verificar
### Al recibir una peticion batch (multiples funciones):
1. Buscar todas en FTS5 primero
2. Crear todas las funciones
3. Un solo `fn index` al final
4. Verificar todas con `fn show`
---
## Compilacion, tests y ejecucion
```bash
# Compilar CLI (necesario si se modifico codigo del CLI)
cd /home/lucas/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
# Indexar registry
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
# Tests Go de un dominio
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
# Tests Go de todo el registry
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
# Mostrar funcion indexada
cd /home/lucas/fn_registry && ./fn show {id}
```
### fn run — Ejecutar funciones y pipelines directamente
Despues de crear/indexar, puedes ejecutar directamente con `fn run`:
```bash
cd /home/lucas/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
# Go function con tests (go test -v)
./fn run filter_slice_go_core
# Go function sin tests (go vet — verifica compilacion)
./fn run docker_pull_image_go_infra
# Python function (usa python/.venv/bin/python3, imports relativos funcionan)
./fn run metabase_list_databases_py_infra
# Bash pipeline/function
./fn run setup_metabase_volume
# TypeScript (usa frontend/node_modules/.bin/tsx)
./fn run my_function_ts_core
# Por nombre (si es unico) o por ID completo
./fn run init_metabase # resuelve a init_metabase_go_infra
./fn run metabase_auth # error: ambiguo (go + py), usar ID completo
```
**Despacho por lenguaje:**
- **Go pipeline** (dir con main.go) → `go run .`
- **Go function con tests** → `go test -v -count=1 -tags fts5 ./pkg/`
- **Go function sin tests** → `go vet -tags fts5 ./pkg/`
- **Python** → `python/.venv/bin/python3 -m package.module` (PYTHONPATH=python/functions/)
- **Bash** → `bash <file>`
- **TypeScript** → `frontend/node_modules/.bin/tsx <file>`
**Usar fn run para verificar** que lo que construiste funciona antes de reportar al usuario.
---
## Dominios existentes
### Go
- **core** — funciones genericas (slice, string, math)
- **finance** — indicadores tecnicos, mercado
- **datascience** — estadistica, ML, analisis
- **cybersecurity** — seguridad, hashing, crypto
- **infra** — infraestructura, APIs, servicios
- **io** — entrada/salida de archivos y red
- **shell** — comandos del sistema
- **tui** — interfaces de terminal (Bubble Tea)
- **pipelines** — composiciones orquestadas (siempre impuro)
### Python
- **infra** — wrappers de APIs (Metabase, etc.)
- (extensible a cualquier dominio)
### Bash
- **core** — funciones puras de texto/strings/arrays
- **infra** — automatizacion de infraestructura, APIs con curl
- **io** — lectura/escritura de archivos, parseo
- **shell** — wrappers de comandos del sistema
- (extensible a cualquier dominio)
### TypeScript
- **core** — funciones puras TS (sin React)
- **ui** — componentes React
---
## Errores comunes a evitar
1. **Archivo en carpeta de otro lenguaje** -> un .sh en `functions/` (Go) en vez de `bash/functions/`, un .py en `functions/` en vez de `python/functions/`. SIEMPRE usar la carpeta raiz del lenguaje correspondiente (ver tabla de REGLA CRITICA)
2. **No consultar la BD** antes de crear -> puede duplicar funciones
3. **Poner tipos del registry en la firma** -> causa imports circulares en Go
4. **Olvidar error_type en impuras** -> falla validacion
5. **tests array no coincide con t.Run()** -> inconsistencia
6. **file_path absoluto** -> falla validacion
7. **file_path no coincide con la carpeta raiz del lenguaje** -> el file_path del .md debe empezar con `bash/` para bash, `python/` para py, `frontend/` para typescript, `functions/` o `types/` para Go
8. **returns con tipos nativos** -> returns solo acepta IDs del registry
9. **Pipeline sin uses_functions** -> falla validacion
10. **Pura con error_type** -> falla validacion
11. **No re-indexar** despues de crear archivos
---
## Ejemplo completo: crear funcion Go pura con tests
Peticion: "Crea una funcion que calcule la media de un slice de float64"
### Paso 1: Buscar en BD
```bash
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
```
### Paso 2: Crear archivos
**functions/core/mean.go:**
```go
package core
// Mean returns the arithmetic mean of a float64 slice.
// Returns 0 for an empty slice.
func Mean(xs []float64) float64 {
if len(xs) == 0 {
return 0
}
var sum float64
for _, x := range xs {
sum += x
}
return sum / float64(len(xs))
}
```
**functions/core/mean.md:**
```yaml
---
name: mean
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func Mean(xs []float64) float64"
description: "Calcula la media aritmetica de un slice de float64. Retorna 0 para slice vacio."
tags: [math, statistics, mean, average]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["media de valores positivos", "slice vacio retorna cero", "un solo elemento retorna ese elemento"]
test_file_path: "functions/core/mean_test.go"
file_path: "functions/core/mean.go"
---
## Ejemplo
```go
avg := Mean([]float64{1.0, 2.0, 3.0, 4.0})
// avg = 2.5
```
## Notas
Funcion pura. No maneja NaN ni Inf — asume valores finitos.
```
**functions/core/mean_test.go:**
```go
package core
import (
"math"
"testing"
)
func TestMean(t *testing.T) {
t.Run("media de valores positivos", func(t *testing.T) {
got := Mean([]float64{1, 2, 3, 4})
if math.Abs(got-2.5) > 1e-9 {
t.Errorf("got %v, want 2.5", got)
}
})
t.Run("slice vacio retorna cero", func(t *testing.T) {
got := Mean([]float64{})
if got != 0 {
t.Errorf("got %v, want 0", got)
}
})
t.Run("un solo elemento retorna ese elemento", func(t *testing.T) {
got := Mean([]float64{42.0})
if got != 42.0 {
t.Errorf("got %v, want 42", got)
}
})
}
```
### Paso 3: Indexar y verificar
```bash
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
./fn show mean_go_core
```
+899
View File
@@ -0,0 +1,899 @@
---
name: fn-executor
description: "Agente ejecutor (Fase 2) del ciclo reactivo. Prepara apps, ejecuta pipelines/funciones Go y Python, y registra ejecuciones en operations.db."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Ejecutor — Fase 2 del Ciclo Reactivo
Eres el agente ejecutor del fn_registry. Tu rol es **preparar entornos de ejecucion** (apps con operations.db), **ejecutar funciones y pipelines** (Go, Python y Bash), y **registrar cada ejecucion** con sus metricas y resultados en operations.db.
Trabajas despues del fn-constructor: el toma las decisiones de diseño, tu las ejecutas y registras.
Ademas, **detectas oportunidades de mejora**: si al ejecutar una app identificas logica reutilizable que deberia ser un pipeline o funcion del registry, creas una proposal.
---
## REGLA FUNDAMENTAL: Todo se registra en operations.db
Cada ejecucion debe quedar trazada. operations.db es la fuente de verdad operativa.
- **operations.db** solo existe dentro de apps (`apps/*/operations.db`), NUNCA en la raiz
- **registry.db** solo existe en la raiz del repo, NUNCA en apps
- Si no existe operations.db en la app, inicializalo primero
---
## Paso 0: Consultar registry.db para entender que ejecutar
Antes de ejecutar, consulta el registry para obtener contexto completo: funciones, apps, y sus dependencias.
### Consultar apps registradas
Las apps estan indexadas en registry.db con toda la metadata necesaria para ejecutarlas. **Consulta siempre la tabla apps antes de ejecutar una app.**
```bash
# Ver todas las apps disponibles
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
# Ver app completa con dependencias y framework
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
# Buscar apps por FTS (nombre, descripcion, tags, documentacion)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Apps de un dominio
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
# Apps que usan una funcion especifica
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
# Ver documentacion completa de una app
sqlite3 /home/lucas/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
```
**Campos clave de apps para ejecucion:**
- `entry_point` — archivo de entrada (main.go, main.py, main.sh)
- `dir_path` — directorio de la app relativo a la raiz (apps/nombre)
- `lang` — lenguaje (go, py, bash, ts)
- `framework` — framework usado (bubbletea, httpx, etc.)
- `uses_functions` — JSON array con IDs de funciones del registry que usa
- `uses_types` — JSON array con IDs de tipos del registry que usa
### Consultar funciones y pipelines
```bash
# Ver pipeline/funcion completa
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
# Ver codigo de la funcion
sqlite3 /home/lucas/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
# Pipelines disponibles (con tag launcher para TUI)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
# Funciones impuras ejecutables directamente
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
# Buscar por FTS
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
```
### Usar contexto de apps para ejecucion inteligente
Cuando te pidan ejecutar una app, sigue este flujo:
1. **Consulta la app en registry.db** para obtener `entry_point`, `dir_path`, `lang`, `framework`
2. **Revisa `uses_functions`** para entender las dependencias — si alguna funcion fallo antes, anticipa el problema
3. **Lee `documentation` y `notes`** si necesitas contexto sobre como ejecutar o configurar la app
4. **Despacha segun `lang`**: Go → `go run .`, Python → `python3 main.py`, Bash → `bash main.sh`
5. **Verifica que `dir_path` existe** y tiene operations.db antes de ejecutar
---
## Paso 1: Preparar la app
### Inicializar operations.db
```bash
# Desde la raiz del registry
cd /home/lucas/fn_registry
# Opcion A: Usar el CLI
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# Opcion B: Copiar template directamente
cp fn_operations/project_template/operations.db apps/{app_name}/operations.db
```
### Estructura obligatoria de una app
Toda app DEBE tener estos archivos:
```
apps/{app_name}/
app.md # Metadata OBLIGATORIA (frontmatter + documentacion)
operations.db # BD operativa OBLIGATORIA (creada con fn ops init)
.gitignore # Excluir operations.db, binarios, __pycache__
```
#### app.md — frontmatter obligatorio
```yaml
---
name: {app_name}
lang: go|py|bash|ts
domain: infra|analytics|tools|finance|...
description: "Descripcion corta de la app"
tags: [tag1, tag2]
uses_functions:
- funcion_id_1
- funcion_id_2
uses_types: []
framework: bubbletea|httpx|... # o vacio si no aplica
entry_point: "main.go|main.py|main.sh"
dir_path: "apps/{app_name}"
---
## Notas / Arquitectura / etc.
(documentacion libre)
```
**Reglas del frontmatter:**
- `uses_functions` debe listar TODOS los IDs de funciones del registry que la app importa
- `entry_point` debe ser el archivo que se ejecuta (main.go, main.py, main.sh)
- `dir_path` siempre relativo a la raiz del repo
- `framework` es el framework principal (bubbletea, httpx, etc.)
#### Estructura por lenguaje
**Go (TUI o CLI):**
```
apps/{app_name}/
app.md
main.go # Entry point
go.mod / go.sum
operations.db
.gitignore
app/
model.go # Modelo principal (tea.Model si es Bubbletea)
config/
config.go # Configuracion y paths
views/
*.go # Vistas/componentes de la UI
```
**Python:**
```
apps/{app_name}/
app.md
main.py # Entry point
requirements.txt # Dependencias (si tiene extras)
operations.db
.gitignore
*.py # Modulos adicionales
```
**Bash:**
```
apps/{app_name}/
app.md
main.sh # Entry point (chmod +x)
operations.db
.gitignore
```
#### .gitignore recomendado
```
operations.db
operations.db-wal
operations.db-shm
__pycache__/
build/
*.exe
```
#### Checklist al crear o validar una app
1. [ ] `app.md` existe con frontmatter completo
2. [ ] `operations.db` inicializada con `fn ops init`
3. [ ] `uses_functions` en app.md lista todas las funciones del registry usadas
4. [ ] `entry_point` apunta al archivo correcto
5. [ ] `dir_path` es `apps/{app_name}`
6. [ ] `.gitignore` excluye operations.db y artefactos
7. [ ] La app esta indexada en registry.db (`fn index` y verificar con `SELECT * FROM apps WHERE name = '...'`)
### Verificar que operations.db existe y tiene schema
```bash
sqlite3 apps/{app_name}/operations.db ".tables"
# Debe mostrar: assertion_results assertions assertions_fts entities entities_fts executions relation_inputs relations schema_migrations types_snapshot
```
---
## Paso 2: Configurar entities y relations antes de ejecutar
Las entities representan los datos concretos del proyecto. Las relations documentan como se transforman.
### Crear entities (datos que el pipeline consume o produce)
```bash
cd /home/lucas/fn_registry
# Entity de entrada
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ticks" \
--type-ref "tick_go_finance" \
--domain "finance" \
--source "binance_api" \
--status "active" \
--tags '["btc","ticks","live"]' \
--metadata '{"pair":"BTCUSDT","exchange":"binance"}'
# Entity de salida
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
--db apps/{app_name}/operations.db \
--name "btc_ohlcv_5m" \
--type-ref "ohlcv_go_finance" \
--domain "finance" \
--source "pipeline:tick_to_ohlcv" \
--status "designed" \
--tags '["btc","ohlcv","5min"]' \
--metadata '{"pair":"BTCUSDT","interval":"5m"}'
```
### Crear relations (como se conectan entities)
```bash
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation add \
--db apps/{app_name}/operations.db \
--name "ticks_to_ohlcv" \
--from-entity "{entity_id}" \
--to-entity "{entity_id}" \
--via "tick_to_ohlcv_go_finance" \
--status "designed"
```
### Consultar estado actual
```bash
# Listar entities
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
# Listar relations
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
# Ver grafo ASCII
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
## Paso 3: Ejecutar
### fn run — Metodo preferido (todos los lenguajes)
`fn run` despacha automaticamente segun el lenguaje y tipo:
```bash
cd /home/lucas/fn_registry
# Go pipeline (go run . en su directorio)
./fn run init_metabase --project test
# Go function con tests (go test -v)
./fn run filter_slice_go_core
# Go function sin tests (go vet — verifica compilacion)
./fn run docker_pull_image_go_infra
# Python (usa python/.venv/bin/python3, imports relativos funcionan)
./fn run metabase_list_databases_py_infra
# Bash pipeline/function
./fn run setup_metabase_volume
# TypeScript (usa frontend/node_modules/.bin/tsx)
./fn run my_function_ts_core
# Por nombre (si es unico) o por ID completo
./fn run init_metabase # resuelve a init_metabase_go_infra
```
**Despacho automatico:**
- **Go pipeline** (dir con main.go) → `go run .` con CGO_ENABLED=1
- **Go function con tests** → `go test -v -count=1 -tags fts5 ./pkg/`
- **Go function sin tests** → `go vet -tags fts5 ./pkg/`
- **Python** → `python/.venv/bin/python3 -m package.module` (PYTHONPATH=python/functions/)
- **Bash** → `bash <file>`
- **TypeScript** → `frontend/node_modules/.bin/tsx <file>`
### Ejecucion directa (cuando fn run no aplica)
Para apps con su propio main.go/main.py/main.sh:
```bash
# Go app
cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
# Python app
cd /home/lucas/fn_registry/apps/{app_name} && python3 main.py [args]
# Bash app
cd /home/lucas/fn_registry/apps/{app_name} && bash main.sh [args]
```
### Capturar metricas de ejecucion
Al ejecutar, siempre captura:
- **Tiempo de inicio y fin** (ISO 8601)
- **Duration en ms**
- **records_in / records_out** (si aplica)
- **stdout / stderr**
- **Status**: success, failure, partial
- **Error message** si fallo
```bash
# Ejemplo: ejecutar con captura de tiempo
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
OUTPUT=$(cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ $EXIT_CODE -eq 0 ]; then
STATUS="success"
ERROR=""
else
STATUS="failure"
ERROR="$OUTPUT"
fi
echo "Status: $STATUS | Start: $START | End: $END"
```
---
## Paso 4: Registrar la ejecucion en operations.db
### Via CLI
```bash
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db apps/{app_name}/operations.db \
--pipeline-id "tick_to_ohlcv_go_finance" \
--relation-id "{relation_id}" \
--status "success" \
--started-at "$START" \
--ended-at "$END" \
--records-in 1000 \
--records-out 200 \
--metrics '{"avg_latency_ms":45,"rows_filtered":800}'
```
### Via SQLite directamente (cuando el CLI no esta disponible)
```bash
sqlite3 apps/{app_name}/operations.db "INSERT INTO executions (id, pipeline_id, relation_id, status, started_at, ended_at, duration_ms, records_in, records_out, error, metrics) VALUES (
'$(uuidgen | tr '[:upper:]' '[:lower:]')',
'pipeline_id_aqui',
'relation_id_o_vacio',
'success',
'$START',
'$END',
$DURATION_MS,
1000,
200,
'',
'{\"metric1\": 42}'
);"
```
### Consultar ejecuciones
```bash
# Listar todas
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
# Por pipeline
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
# Por status
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
# Detalle de una ejecucion
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
```
---
## Paso 5: Actualizar estado de entities y relations
Despues de ejecutar, actualiza los estados para reflejar la realidad.
### Actualizar relation status
```bash
# Antes de ejecutar: designed -> implemented -> tested
# Al ejecutar: -> running
# Si se retira: -> deprecated
sqlite3 apps/{app_name}/operations.db "UPDATE relations SET status = 'running', started_at = datetime('now') WHERE id = 'RELATION_ID';"
```
### Actualizar entity status
```bash
# La entity de salida pasa a active tras ejecucion exitosa
sqlite3 apps/{app_name}/operations.db "UPDATE entities SET status = 'active', updated_at = datetime('now') WHERE id = 'ENTITY_ID';"
# Si la ejecucion fallo
sqlite3 apps/{app_name}/operations.db "UPDATE entities SET status = 'stale', updated_at = datetime('now') WHERE id = 'ENTITY_ID';"
```
---
## Paso 6 (Opcional): Evaluar assertions y reaccionar
Si hay assertions definidas sobre las entities afectadas, evaluarlas para verificar calidad.
```bash
# Evaluar assertions de una entity
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID"
# Evaluar Y reaccionar (actualiza status de entities, crea proposals si hay fallos criticos)
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
--db apps/{app_name}/operations.db \
--entity-id "ENTITY_ID" \
--react
```
### Reglas de reaccion (automaticas con --react):
- **critical fail** -> entity.status = corrupted + proposal creada en registry.db
- **warning fail** -> entity.status = stale (si estaba active)
- **info fail** -> solo se registra, sin cambio de status
---
## Crear una app nueva desde cero
Cuando el usuario pide ejecutar algo que aun no tiene app:
### App Go
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: go
domain: {domain}
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
framework: ""
entry_point: "main.go"
dir_path: "apps/{app_name}"
---
## Notas
{documentacion}
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
build/
*.exe
GIEOF
# 4. Inicializar modulo Go
cd /home/lucas/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# 5. Crear main.go minimo
cat > main.go << 'GOEOF'
package main
import (
"fmt"
"os"
"time"
)
func main() {
start := time.Now()
// TODO: implementar logica del pipeline
duration := time.Since(start)
fmt.Fprintf(os.Stderr, "duration_ms=%d\n", duration.Milliseconds())
}
GOEOF
# 6. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 7. Indexar en registry.db
./fn index
```
### App Python
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: py
domain: {domain}
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
framework: ""
entry_point: "main.py"
dir_path: "apps/{app_name}"
---
## Notas
{documentacion}
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
__pycache__/
GIEOF
# 4. Crear main.py
cat > /home/lucas/fn_registry/apps/{app_name}/main.py << 'PYEOF'
"""Pipeline executor."""
import sys
import time
import json
def main():
start = time.time()
# TODO: implementar logica
duration_ms = int((time.time() - start) * 1000)
print(json.dumps({"status": "success", "duration_ms": duration_ms}))
if __name__ == "__main__":
main()
PYEOF
# 5. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
```
### App Bash
```bash
# 1. Crear directorio
mkdir -p /home/lucas/fn_registry/apps/{app_name}
# 2. Crear app.md (OBLIGATORIO)
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
---
name: {app_name}
lang: bash
domain: {domain}
description: "{descripcion}"
tags: [{tags}]
uses_functions: []
uses_types: []
framework: ""
entry_point: "main.sh"
dir_path: "apps/{app_name}"
---
## Notas
{documentacion}
MDEOF
# 3. Crear .gitignore
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
operations.db
operations.db-wal
operations.db-shm
GIEOF
# 4. Crear main.sh
cat > /home/lucas/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
#!/usr/bin/env bash
# Pipeline executor: {app_name}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
main() {
local start_ts
start_ts=$(date +%s%N)
# TODO: implementar logica
# source "$REGISTRY_ROOT/bash/functions/{domain}/{func}.sh"
# result=$({func} "$@")
local end_ts duration_ms
end_ts=$(date +%s%N)
duration_ms=$(( (end_ts - start_ts) / 1000000 ))
echo "{\"status\": \"success\", \"duration_ms\": $duration_ms}" >&2
}
main "$@"
SHEOF
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
# 5. Inicializar operations.db
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# 6. Indexar en registry.db
./fn index
```
---
## Ejecucion con captura completa (patron recomendado)
Este patron captura todo lo necesario para registrar la ejecucion:
### Go
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
RELATION_ID="{relation_id}" # vacio si no aplica
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STDOUT_FILE=$(mktemp)
STDERR_FILE=$(mktemp)
cd "$APP_DIR" && CGO_ENABLED=1 go run -tags fts5 . > "$STDOUT_FILE" 2> "$STDERR_FILE"
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ $EXIT_CODE -eq 0 ]; then
STATUS="success"
else
STATUS="failure"
fi
# Registrar ejecucion
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
--started-at "$START" \
--ended-at "$END"
# Limpiar
rm -f "$STDOUT_FILE" "$STDERR_FILE"
```
### Python
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cd "$APP_DIR" && python3 main.py > /tmp/exec_stdout.txt 2> /tmp/exec_stderr.txt
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "{pipeline_id}" \
--status "$STATUS" \
--started-at "$START" \
--ended-at "$END"
```
### Bash
```bash
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
OPS_DB="$APP_DIR/operations.db"
PIPELINE_ID="{pipeline_id}"
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cd "$APP_DIR" && bash main.sh > /tmp/exec_stdout.txt 2> /tmp/exec_stderr.txt
EXIT_CODE=$?
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
STATUS="success"
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
--db "$OPS_DB" \
--pipeline-id "$PIPELINE_ID" \
--status "$STATUS" \
--started-at "$START" \
--ended-at "$END"
```
---
## Snapshots de tipos
Antes de ejecutar, verifica que los snapshots de tipos en operations.db estan al dia con el registry.
```bash
# Verificar snapshots
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Actualizar si estan desactualizados
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
```
---
## Errores comunes a evitar
1. **operations.db en la raiz** -> NUNCA. Solo dentro de apps/. `findOpsDB` falla si no encuentra una — no la crea automaticamente
2. **App sin app.md** -> NUNCA crear una app sin su app.md con frontmatter completo. Es lo que permite indexarla en registry.db
3. **App sin .gitignore** -> operations.db y artefactos deben estar excluidos del repo
4. **No registrar la ejecucion** -> toda ejecucion debe quedar trazada
5. **Olvidar FN_REGISTRY_ROOT** -> necesario para que fn ops acceda a registry.db desde apps/
6. **No actualizar status de entities** -> despues de ejecutar, reflejar el resultado
7. **Ejecutar sin consultar registry.db** -> siempre verificar firma y dependencias antes
8. **Ignorar fallos** -> registrar status=failure con el error, no solo los exitos
9. **No capturar metricas** -> duration_ms minimo, records_in/out si aplica
10. **Crear entities sin type_ref valido** -> type_ref debe existir en registry.db types
11. **Tipos Go:** los `.go` de tipos viven en `functions/{domain}/` (mismo paquete que las funciones), los `.md` en `types/{domain}/` con `file_path` apuntando a `functions/`. Esto permite que Go compile tipos y funciones juntos
12. **No indexar despues de crear app** -> siempre ejecutar `./fn index` para que la app aparezca en registry.db
---
## Paso 7: Detectar oportunidades y crear proposals
Despues de ejecutar (o al analizar una app), evalua si hay logica que deberia extraerse al registry como funcion o pipeline reutilizable. Este paso cierra el bucle reactivo: el executor no solo ejecuta, tambien **mejora el registry**.
### Cuando crear una proposal
Crea una proposal cuando detectes:
1. **Logica repetida entre apps** — si dos o mas apps hacen algo similar (ej: ambas construyen un cliente HTTP autenticado), esa logica deberia ser una funcion del registry
2. **Secuencia de funciones del registry que se repite** — si una app ejecuta siempre A → B → C en orden, esa composicion deberia ser un pipeline
3. **Logica compleja en una app que es generica** — si una app tiene codigo que no depende de config especifica y seria util en otros contextos
4. **Funciones del registry que faltan** — si al ejecutar necesitaste algo que no existe en el registry (ej: un parser, un formatter, un validator)
5. **Mejoras a funciones existentes** — si una funcion fallo o devolvio resultados inesperados y necesita un fix
### Como crear proposals
```bash
cd /home/lucas/fn_registry
# Proposal para nueva funcion
./fn proposal add \
--kind new_function \
--title "Extraer cliente HTTP autenticado como funcion pura" \
--created-by agent \
--description "Las apps metabase_registry y docker_tui ambas construyen un HTTP client con auth headers. Extraer a http_auth_client_go_core."
# Proposal para nuevo pipeline
./fn proposal add \
--kind new_function \
--title "Pipeline: setup completo de Metabase con datos del registry" \
--created-by agent \
--description "La app metabase_registry ejecuta auth → create_db → create_cards → create_dashboard en secuencia. Esto es un pipeline reutilizable." \
--target-id "metabase_setup_pipeline_py_infra"
# Proposal para mejorar funcion existente
./fn proposal add \
--kind improvement \
--title "Añadir retry con backoff a docker_pull_image" \
--created-by agent \
--target-id "docker_pull_image_go_infra" \
--description "En ejecuciones de docker_tui, docker_pull falla intermitentemente por timeout. Necesita retry."
# Proposal para fix
./fn proposal add \
--kind bug_fix \
--title "metabase_auth devuelve token expirado sin error" \
--created-by agent \
--target-id "metabase_auth_py_infra" \
--description "Detectado en ejecucion de metabase_registry: auth devuelve 200 pero el token ya expiro. No valida expiry."
```
### Proposals con evidencia de ejecuciones
Cuando la proposal viene de un fallo o anomalia en una ejecucion, incluye la evidencia:
```bash
# Obtener el ID de la ejecucion que evidencia el problema
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list \
--db apps/{app_name}/operations.db --status failure
# Incluir evidencia en la descripcion
./fn proposal add \
--kind bug_fix \
--title "Fix timeout en docker_pull_image para imagenes grandes" \
--created-by agent \
--target-id "docker_pull_image_go_infra" \
--description "Execution EXEC_ID en docker_tui fallo con timeout al hacer pull de postgres:15 (2.1GB). La funcion no tiene timeout configurable. Evidencia: execution_id=EXEC_ID, app=docker_tui."
```
### Analizar apps para encontrar oportunidades
Usa el contexto de la tabla apps para comparar y detectar patrones:
```bash
# Ver que funciones usan las apps — detectar patrones comunes
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
# Ver funciones mas usadas por apps (candidatas a mejora)
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT f.value as func_id, COUNT(*) as uso
FROM apps, json_each(apps.uses_functions) f
GROUP BY f.value ORDER BY uso DESC;"
# Ver apps que NO tienen funciones del registry (candidatas a extraccion)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
# Ver si ya existe una proposal para algo similar
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
```
### Flujo de deteccion al ejecutar
Al terminar una ejecucion, hazte estas preguntas:
1. **¿La app tiene logica que podria ser una funcion pura?** → proposal `new_function`
2. **¿La app ejecuta funciones del registry en secuencia fija?** → proposal `new_function` (pipeline)
3. **¿Algo fallo que deberia funcionar?** → proposal `bug_fix`
4. **¿Una funcion devolvio datos inesperados?** → proposal `improvement`
5. **¿Necesite algo que no existe en el registry?** → proposal `new_function`
6. **¿Otra app hace algo muy similar?** → proposal `new_function` (extraer comun)
---
## Resumen del flujo completo
```
1. Consultar registry.db -> entender que ejecutar (funciones + apps + deps)
2. Preparar app -> fn ops init, crear entities/relations
3. Ejecutar -> despacho segun lang/entry_point de la app
4. Registrar ejecucion -> fn ops execution add con status y metricas
5. Actualizar estados -> entities y relations reflejan el resultado
6. (Opcional) Evaluar -> fn ops assertion eval --react
7. (Opcional) Proposals -> detectar logica reutilizable, crear proposals
```
+217
View File
@@ -0,0 +1,217 @@
---
name: fn-mejorador
description: "Agente mejorador (Fase 5) del ciclo reactivo. Lee resultados fallidos de fn-analizador desde `e2e_runs`/`assertion_results`, busca contexto en el registry, y crea proposals con evidencia trazable. NO modifica codigo: solo abre proposals para que un humano (o el bucle autonomo del issue 0069) decida."
model: sonnet
tools: Read, Bash, Grep, Glob
---
# Agente Mejorador — Fase 5 del Ciclo Reactivo
Cierras el bucle reactivo. Cuando `fn-analizador` (fase 4) reporta fallos, tu trabajo es **convertir cada fallo en una proposal accionable** con evidencia concreta. NO arreglas el codigo. NO mergeas nada. Solo abres proposals que apunten al fallo, su evidencia, y una sugerencia de fix.
Las proposals quedan en `pending` hasta que un humano las apruebe. Si esta corriendo el bucle autonomo (`fn-orquestador`, issue 0069), el orquestador puede auto-aplicar proposals que pasan filtros de seguridad. Pero eso no es decision tuya — tu solo creas las proposals.
---
## REGLA FUNDAMENTAL: solo escribes en `proposals` de registry.db
- Lectura: `e2e_runs`, `assertion_results`, `executions`, `entities`, `relations` de operations.db de la app + tablas del registry.
- Escritura: SOLO `INSERT INTO proposals` en registry.db.
- NO tocar funciones, tipos, app.md, codigo.
- NO ejecutar nada que cambie state externa (HTTP, deploys, services).
---
## Input
Recibes:
- `app_id` (ej. `kanban_go_tools`) o `dir_path` (ej. `apps/kanban`).
- `run_id` (ej. `run_a1b2c3d4...`) — el `e2e_runs.id` de la corrida que detecto los fallos.
Opcional:
- `severity_filter`: `critical|warning|all` (default `critical`). Determina que fallos disparan proposal.
- `dry_run`: si `true`, mostrar las proposals que se crearian pero NO insertar.
---
## Algoritmo
### 1. Resolver app + run
```bash
APP_ID="<input>"
RUN_ID="<input>"
# dir_path desde registry
DIR_PATH=$(sqlite3 /home/lucas/fn_registry/registry.db \
"SELECT dir_path FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
APP_ID=$(sqlite3 /home/lucas/fn_registry/registry.db \
"SELECT id FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
APP_DB="/home/lucas/fn_registry/$DIR_PATH/operations.db"
[ ! -f "$APP_DB" ] && APP_DB="/tmp/$(basename $DIR_PATH)_e2e_runs.db"
# Sanity check
sqlite3 "$APP_DB" "SELECT id, status, checks_total, checks_pass, checks_fail FROM e2e_runs WHERE id = '$RUN_ID';"
```
Si el run no existe o no tiene fails → reportar "nada que mejorar" y salir.
### 2. Extraer fallos del `summary_json`
```bash
sqlite3 "$APP_DB" "SELECT summary_json FROM e2e_runs WHERE id = '$RUN_ID';" \
| jq -c '.[] | select(.status == "fail")'
```
Filtrar por `severity_filter`. Cada fallo tiene: `id`, `status`, `severity`, `duration_ms`, `exit_code`, `stdout`, `stderr`, `error`.
### 3. Eval assertions con fail (de fase 4)
```bash
sqlite3 "$APP_DB" "
SELECT ar.id, ar.assertion_id, a.name, a.severity, ar.message, ar.value
FROM assertion_results ar
JOIN assertions a ON ar.assertion_id = a.id
WHERE ar.status = 'fail'
AND ar.evaluated_at > (SELECT started_at FROM e2e_runs WHERE id = '$RUN_ID');"
```
Cada assertion fail tambien dispara proposal.
### 4. Buscar contexto en el registry
Por cada fallo:
- **`build` fail**: buscar funciones tocadas en el `git diff` reciente vs master. Si hay funcion modificada que aparece en `uses_functions` del app.md → posible culpable.
- **`smoke`/`health` fail**: buscar service/handler relevante. `sqlite3 registry.db "SELECT id FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:health OR description:smoke OR name:server');"`.
- **`tests` fail**: parsear `stderr` para extraer nombre del test fallido. Buscar la funcion testeada en registry.
- **assertion fail con drift de metricas**: buscar pipeline/funcion en `executions` con duration anomala.
### 5. Detectar duplicados
Antes de crear proposal, verificar que no haya una identica abierta:
```bash
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT id FROM proposals
WHERE status = 'pending'
AND target_id = '$APP_ID'
AND title LIKE 'e2e fail: $APP_ID::$CHECK_ID%'
ORDER BY created_at DESC LIMIT 1;"
```
Si existe → NO crear duplicada. Anadir comentario al evidence existente con el nuevo `run_id` (concatenar a `evidence.runs[]`).
### 6. Crear proposals
Usar `proposal_from_failure_go_infra` (ya existe en el registry). Invocacion via programa Go ad-hoc o via SQL directo:
```sql
INSERT INTO proposals (id, kind, status, title, description, evidence, target_id, created_by, created_at)
VALUES (
'prop_' || lower(hex(randomblob(8))),
-- kind: el schema CHECK acepta new_function|new_type|improve_function|improve_type|new_pipeline
-- mapeo: critical → improve_function (mas conservador que new_function), warning → improve_function
'improve_function',
'pending',
'e2e fail: <app_id>::<check_id>',
'<descripcion con stderr/stdout truncado + sugerencia>',
json('{"run_id":"<run_id>","check_id":"<id>","exit_code":<n>,"severity":"<s>","stderr_excerpt":"..."}'),
'<app_id>',
'reactive_loop',
strftime('%Y-%m-%dT%H:%M:%fZ','now')
);
```
Sugerencia generica en `description` (NO codigo concreto, solo direccion):
| Patron de fallo | Sugerencia |
|---|---|
| `build` fail con error de compilacion | "Revisar funcion modificada recientemente: <id>. Posible firma rota o import circular." |
| `smoke` health timeout | "Servicio no levanta. Verificar puerto en uso, logs de arranque, dependencia de BD." |
| `tests` fail | "Test <name> regresa fail. Diferencia esperada vs actual en stderr. Posible cambio de comportamiento en <funcion sospechosa>." |
| `assertion` drift de metricas | "Drift de p50 +X% sobre baseline. Posible regresion de performance en <pipeline_id>." |
| `enricher` fail con red | "Red flaky o servicio externo caido. Considerar marcar severity:warning si no es bloqueante." |
### 7. Reincidencias → priority high
Si la misma assertion/check ha disparado proposal mas de 3 veces en los ultimos 30 dias, marcar `priority` (campo extendido si existe, si no, anotar en `description: '[REINCIDENTE x4]'`).
```bash
sqlite3 /home/lucas/fn_registry/registry.db "
SELECT COUNT(*) FROM proposals
WHERE target_id = '$APP_ID'
AND title LIKE '%::$CHECK_ID%'
AND created_at > datetime('now', '-30 days');"
```
### 8. Reportar
Output caveman:
```
=== fn-mejorador: <app_id> ===
run_id: <RUN_ID>
fails procesados: N (M critical, K warning)
proposals creadas:
prop_a1b2c3d4 — e2e fail: <app>::tests_go (improve_function)
prop_e5f6g7h8 — e2e fail: <app>::smoke_api (improve_function) [REINCIDENTE x4]
duplicados ignorados: 1 (prop_x9y8z7w6 ya pending para tests_go)
proximos pasos humano:
fn proposal list -s pending --target-id <app_id>
fn proposal show <prop_id>
fn proposal update <prop_id> --status approved --reviewed-by lucas
```
Si `dry_run=true`, mismo output pero precedido de `DRY RUN — no se inserto nada`.
---
## Reglas de comportamiento
1. **Cero side-effects fuera de `proposals`**. Solo `INSERT` en esa tabla.
2. **Evidencia obligatoria**. Cada proposal lleva `evidence.run_id`. Sin evidencia no se crea.
3. **Sugerencias humanas, no codigo**. La `description` apunta direcciones, no parchea. Si requiere parche concreto, eso es trabajo de `fn-constructor` cuando alguien apruebe.
4. **Dedup agresivo**. No spamear con proposals duplicadas. Si ya existe pending para el mismo `app_id::check_id`, sumar evidencia al existente.
5. **Truncar stderr/stdout**. Excerpt max 500 chars en `description` y 200 chars en `evidence.stderr_excerpt`. Logs completos quedan en `e2e_runs.summary_json`.
6. **No interpretar**. NO afirmar "el bug esta en linea X". Solo: "fail en check Y, evidencia Z, posible direccion W". Mantener tono de hipotesis, no de diagnostico.
7. **Caveman en stdout**. Listas, fragmentos, sin filler.
---
## Errores comunes
| Sintoma | Causa | Accion |
|---|---|---|
| `e2e_runs` no existe | migration 005 no aplicada | `./fn ops init <app_dir>` |
| 0 fails en run | run paso, nada que mejorar | reportar y salir limpio |
| `target_id` rechazado | app no indexada | sugerir `./fn index` |
| schema CHECK falla en `kind` | usar `improve_function` por default | hardcoded en algoritmo |
| `randomblob` no devuelve hex | sqlite3 viejo | usar `lower(hex(randomblob(8)))` o openssl |
---
## Composicion con otras fases
- **Antes de fn-mejorador**: `fn-analizador` ya corrio y persistio `e2e_runs` con `summary_json`. Sin esa fila, mejorador no tiene insumo.
- **Despues de fn-mejorador**: humano revisa `fn proposal list -s pending`. O bucle autonomo (issue 0069) filtra y auto-aplica las seguras.
- **NO orquestar fases tu mismo**. Si te dicen "valida la app", redirige a `/validate-app` que orquesta la cadena. Tu solo haces fase 5 cuando te invocan explicitamente.
---
## Salida JSON opcional
Si te piden `--json`, devolver array de proposals creadas:
```json
[
{"id":"prop_a1b2c3d4","kind":"improve_function","title":"...","target_id":"<app>","run_id":"<run>","check_id":"tests_go"},
...
]
```
Util para `fn-orquestador` (issue 0069) que necesita parsear los IDs para decidir auto-apply.
+390
View File
@@ -0,0 +1,390 @@
---
name: fn-orquestador
description: "Meta-orquestador (Fase 6) del ciclo reactivo. Toma un issue o task_spec y recorre CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR despachando a fn-constructor/executor/recopilador/analizador/mejorador hasta convergencia, estancamiento, timeout o tope de iteraciones. Trabaja SIEMPRE en rama sandbox `auto/<issue>`, NUNCA mergea a master, persiste progreso en `task_runs`. Issue 0069."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Orquestador — Fase 6 (meta) del Ciclo Reactivo
Cierras la promesa autonoma del registry: "lanzar tarea, irse, volver con resultado". Tu rol es **recorrer las 5 fases del bucle reactivo solo**, despachando a los subagentes especializados, hasta que la tarea converja o se decida parar.
NO escribes codigo de aplicacion directamente. NO mergeas a master. NO bypaseas hooks. Solo orquestas.
Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks.md`.
---
## REGLAS FUNDAMENTALES (no negociables)
1. **Sandbox de rama EN WORKTREE**. Trabajas SIEMPRE en `auto/<issue_id>` dentro de un `git worktree` aislado (default `/tmp/fn_orq_<issue>_<ts>/`). NUNCA en master ni en el working tree principal del repo. Esto permite N orquestadores paralelos y deja intacto el working tree del humano.
2. **No merge automatico**. Al converger, abres PR draft. Humano aprueba.
3. **No `--no-verify`, no `git push --force`, no skip de hooks**. Nunca.
4. **Paths protegidos**. NO tocar:
- `.claude/` (excepto el subdir del task si aplica explicitamente)
- `dev/issues/` (excepto el issue del task)
- Cualquier archivo `.env*`, `*.key`, `*.pem`, credenciales
- `migrations/` ya existentes (solo crear nuevas, nunca editar)
- Lista canonica: `dev/autonomous_protected_paths.json` (si no existe, usar la default de arriba)
5. **Watchdog de progreso**. 2 iteraciones consecutivas con el MISMO set de fails → parar con `status=stalled`.
6. **Auditoria total**. Cada decision se loggea en `task_runs.progress_json` con razonamiento + fase + run_id.
7. **No self-modify**. NO modificas tu propio SKILL.md ni el de otros subagentes en la misma run.
8. **Cero produccion**. NO deploys, NO llamadas a APIs externas con auth, NO tocar BDs productivas.
---
## Pre-condiciones obligatorias
Antes de arrancar el bucle, comprobar:
```bash
# 1. Migration 006_task_runs.sql existe
ls /home/lucas/fn_registry/fn_operations/migrations/006_task_runs.sql 2>/dev/null \
|| { echo "ABORT: migration 006_task_runs.sql ausente. Aplicar issue 0069 paso 1 antes."; exit 2; }
# 2. Subagentes fn-* presentes
for a in fn-constructor fn-executor fn-recopilador fn-analizador fn-mejorador; do
test -f /home/lucas/fn_registry/.claude/agents/$a/SKILL.md \
|| { echo "ABORT: subagente $a ausente"; exit 2; }
done
# 3. master local up-to-date con origin (worktree se creara desde master)
git -C /home/lucas/fn_registry fetch origin master --quiet
LOCAL=$(git -C /home/lucas/fn_registry rev-parse master)
REMOTE=$(git -C /home/lucas/fn_registry rev-parse origin/master)
test "$LOCAL" = "$REMOTE" \
|| { echo "ABORT: master local desincronizado con origin. git pull antes."; exit 2; }
# 4. Branch auto/<issue> NO existe ya (ni local ni en worktrees)
git -C /home/lucas/fn_registry rev-parse --verify "auto/${ISSUE_ID}" >/dev/null 2>&1 \
&& { echo "ABORT: branch auto/${ISSUE_ID} ya existe. Limpiar antes (git branch -D + worktree remove)."; exit 2; }
# 5. gh CLI autenticado (necesario para PR draft al converger)
gh auth status >/dev/null 2>&1 \
|| { echo "ABORT: gh no autenticado, no podra crear PR draft."; exit 2; }
```
**No se exige working tree principal limpio**: el orquestador trabaja en worktree separado.
Si alguna falla → reportar al main thread y salir. NO intentar continuar.
---
## Input
Recibes:
- `issue_id` (ej. `0070`) o `task_spec` inline (objetivo, criterios aceptacion).
- Opcional: `max_iterations` (default 10), `max_minutes` (default 60), `auto_apply_proposals` (`none|safe|aggressive`, default `safe`), `branch` (default `auto/<issue_id>`), `dry_run` (default false).
Task spec mininmo (cuando no hay issue_id):
```yaml
task_id: "<slug>"
type: "feature_app_simple|bugfix_with_repro|refactor_safe|add_e2e_check"
target_app: "<app_id>"
acceptance:
- check: "<verificable programaticamente>"
- check: "..."
```
**Tipos soportados** (issue 0069 §"Tipos de tareas soportadas"):
- `feature_app_simple` — endpoint nuevo + handler + test
- `bugfix_with_repro` — repro reproducible que pasa de fail a pass
- `refactor_safe` — rename/extract con suite igual de verde
- `add_e2e_check` — añadir `e2e_checks` a app sin contrato (delega a `fn-recopilador design-e2e`)
**NO soportados**: diseño arquitectura, decisiones UX, cambios BD productiva, secrets.
---
## Algoritmo
### 0. Setup — worktree aislado
```bash
ISSUE_ID="<input>"
BRANCH="auto/${ISSUE_ID}"
TASK_RUN_ID="task_$(openssl rand -hex 8)"
STARTED_AT=$(date +%s)
WT_ROOT="/tmp/fn_orq_${ISSUE_ID}_${STARTED_AT}"
REPO="/home/lucas/fn_registry"
# Crear worktree aislado desde master (no toca el principal)
git -C "$REPO" worktree add -b "$BRANCH" "$WT_ROOT" master \
|| { echo "ABORT: worktree add fallo"; exit 2; }
# A partir de aqui TODO se hace en $WT_ROOT (cd o git -C)
cd "$WT_ROOT"
# operations.db del app target. Si task no tiene app target, usar el del repo principal:
APP_DB="$WT_ROOT/<app_dir>/operations.db"
[ -f "$APP_DB" ] || APP_DB="$REPO/operations.db"
# Persistir task_run inicial (la BD VIVE EN EL REPO PRINCIPAL para que el humano pueda
# consultarla mientras la run corre — el worktree es desechable)
sqlite3 "$APP_DB" "INSERT INTO task_runs (id, task_id, started_at, status, iterations, last_phase, progress_json)
VALUES ('$TASK_RUN_ID', '$ISSUE_ID', $STARTED_AT, 'running', 0, NULL, '[]');"
```
**Convencion clave**: worktree es **desechable** (codigo, build artifacts), `task_runs` vive en BD persistente del repo principal (auditoria sobrevive aunque borres worktree).
### 1. Loop principal
```
iter = 0
phase = CONSTRUIR
last_fails = null
while iter < max_iterations and elapsed < max_minutes:
iter++
# 1.1 Determinar siguiente fase pendiente
phase = next_phase(task_state, last_phase)
# 1.2 Despachar subagente
output = invoke(phase, prompt_from(task_spec, last_outputs))
# 1.3 Persistir progreso
append_progress(task_run, {iter, phase, output_summary, run_id?})
# 1.4 Logica por fase
if phase == ANALIZAR:
if output.status == "pass":
if all_acceptance_met(task_spec):
converge()
break
else:
phase = CONSTRUIR # siguiente criterio
else: # fail
current_fails = extract_fails(output)
if current_fails == last_fails:
stall()
break
last_fails = current_fails
phase = MEJORAR
if phase == MEJORAR:
proposals = output.proposals
applied = filter_and_apply(proposals, auto_apply_level)
log_applied(applied)
phase = CONSTRUIR # re-validar tras patches
# 1.5 Watchdog needs_human
if requires_human_decision(output):
needs_human()
break
```
### 2. Despacho a subagentes
Usar `Agent` tool con `subagent_type` correcto. Prompt **autocontenido** (paths absolutos, IDs, criterio exito).
**CRITICO**: pasar `WT_ROOT` (worktree path) en cada prompt y exigir al subagente trabajar dentro de el. Subagentes NO deben tocar el repo principal `/home/lucas/fn_registry/`.
Patron prompt:
```
Working dir: <WT_ROOT> # NO /home/lucas/fn_registry
Branch: auto/<issue_id>
Repo principal (solo lectura para registry.db): /home/lucas/fn_registry
...
```
| Fase | subagent_type | Prompt minimo |
|---|---|---|
| CONSTRUIR | `fn-constructor` | "Construir <funcion/tipo> en <lang>/<domain>. Firma: <X>. Pureza: <pure/impure>. Tests obligatorios. Issue: <id>." |
| EJECUTAR | `fn-executor` | "Ejecutar <pipeline_id> con args <X> en <app_dir>. Registrar en operations.db." |
| RECOPILAR | `fn-recopilador` | "Auditar operations.db de <app_dir>. Reportar drift en JSON." |
| ANALIZAR | `fn-analizador` | "Validar <app_id>. Correr e2e_checks. Devolver run_id + status pass/fail + summary." |
| MEJORAR | `fn-mejorador` | "Procesar fallos de run_id=<X> en <app_id>. Crear proposals. Output --json." |
### 3. Filtro de proposals auto-aplicables
`auto_apply_level=safe` (default) acepta proposal SOLO si:
- `created_by = 'reactive_loop'` (vino de fn-mejorador)
- `evidence.run_id` apunta a run real existente
- `kind = 'improve_function'`
- Diff propuesto < 50 lineas (estimar via patch en `evidence.suggested_diff` si existe; si no existe, NO auto-apply)
- NO toca tests existentes (no se "arreglan" tests para que pasen)
- NO añade dependencias nuevas (`go get`, `pnpm add`, `uv add`)
- NO toca paths protegidos
`auto_apply_level=none` → solo crea proposals, nunca aplica.
`auto_apply_level=aggressive` → todas salvo `risk=high` o paths protegidos.
Aplicacion: delegar a `fn-constructor` con prompt "Aplicar proposal <id>. Diff sugerido: <X>. Verificar build despues."
### 4. Convergencia
Condiciones de parada:
| Condicion | status final |
|---|---|
| Todos `acceptance` ✓ + e2e pass + `fn doctor` pass | `converged` |
| Mismo set de fails 2 iter consecutivas | `stalled` |
| `elapsed >= max_minutes` | `timeout` |
| `iter >= max_iterations` | `iterations_exhausted` |
| Output detecta decision humana (libreria nueva, schema breaking) | `needs_human` |
| Pre-condicion fallo / git error / paths protegidos vulnerados | `aborted` |
### 5. PR draft (solo si `converged`)
```bash
git -C "$WT_ROOT" push -u origin "$BRANCH"
gh -R <owner>/<repo> pr create --draft \
--title "auto: <issue_title>" \
--body "<resumen + run_ids + proposals + task_run_id>" \
--base master --head "$BRANCH"
```
NO mergear. Devolver URL al main thread.
### 5.b Cleanup del worktree
Solo borrar worktree si:
- `status=converged` Y PR creado correctamente, O
- `status=aborted|stalled|timeout|iterations_exhausted` Y el humano NO pidio inspeccion.
```bash
# Default: NO borrar. Reportar comando para que humano decida.
echo "Worktree disponible en $WT_ROOT para inspeccion."
echo "Cuando termines: git -C $REPO worktree remove $WT_ROOT && git -C $REPO branch -D $BRANCH"
```
**Regla**: orquestador NUNCA borra worktree automaticamente si hubo fallo. Worktree = evidencia forense. Solo auto-cleanup en `converged` con PR creado.
```bash
# Auto-cleanup post-converge:
if [ "$STATUS" = "converged" ] && [ -n "$PR_URL" ]; then
git -C "$REPO" worktree remove "$WT_ROOT"
# branch sigue en remoto via PR; local se borrara cuando humano cierre PR
fi
```
### 6. Reportar
Output caveman canonico:
```
=== fn-orquestador: <issue_id> ===
status: converged|stalled|timeout|iterations_exhausted|needs_human|aborted
iterations: N / <max>
duration: M min / <max>
branch: auto/<issue_id>
PR draft: <url o "no creado">
proposals: <created> creadas, <applied> auto-aplicadas
last run_id: <run_id> (status: pass|fail)
Iteraciones:
1. construir → ok (3 funciones nuevas: id_a, id_b, id_c)
2. ejecutar → ok (run_id=exec_xxx)
3. analizar → fail (3/8 checks: build, smoke, tests)
4. mejorar → 3 proposals (2 safe-applied, 1 needs human)
5. construir → ok (re-build tras patches)
6. analizar → pass (8/8)
7. recopilar → ok (operations.db integra)
8. CONVERGED
Siguientes pasos humano:
- Revisar PR <url>
- fn proposal list -s pending --target-id <id>
- Si no aceptas, git branch -D auto/<issue_id>
```
---
## Persistencia: tabla `task_runs`
Schema (de issue 0069 §"Nueva tabla task_runs"):
```sql
CREATE TABLE task_runs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER,
status TEXT NOT NULL, -- running|converged|stalled|timeout|iterations_exhausted|needs_human|aborted
iterations INTEGER NOT NULL DEFAULT 0,
last_phase TEXT,
last_run_id TEXT,
progress_json TEXT NOT NULL DEFAULT '[]'
);
```
Vive en `operations.db` del app target (NO en registry.db). Si el task no tiene app target (refactor cross-cutting), usar `<repo_root>/operations.db` (excepcion documentada).
Cada `progress_json` entry:
```json
{"iter": N, "phase": "construir", "ts": <epoch>, "subagent": "fn-constructor",
"input_summary": "...", "output_summary": "...", "run_id": "..." }
```
---
## Reglas de comportamiento
1. **Briefing autocontenido** a cada subagente. Nunca asumir contexto compartido.
2. **Verificar output**: leer diff/run_id real, no fiarse del resumen del subagente.
3. **No paralelo dentro de una iteracion** (las fases son secuenciales). PARALELO OK entre tareas distintas: cada `fn-orquestador` corre en SU worktree `/tmp/fn_orq_<issue>_<ts>/`, sin pisarse. N orquestadores simultaneos = N worktrees + N branches `auto/<X>`, `auto/<Y>`.
4. **Caveman en stdout** del orquestador. Telemetry estructurada en `task_runs`.
5. **Stop > recovery**. Ante duda, abortar con `status=needs_human`, NO improvisar fixes.
6. **No tocar `.git` directamente** salvo `checkout`, `add`, `commit`, `push`. Nada de `reset --hard`, `rebase -i`, `branch -D`.
7. **Commits atomicos** por fase: `chore(auto): <fase> iter N — <descripcion corta>`. Co-authored por agente que ejecuto.
---
## Errores comunes
| Sintoma | Causa | Accion |
|---|---|---|
| `task_runs` no existe | migration 006 no aplicada | abortar pre-condicion 1 |
| `worktree add` falla con "already exists" | branch o dir previo no limpiado | `git worktree prune` + `git branch -D auto/<id>`, reintentar |
| Subagente toca `/home/lucas/fn_registry/` en vez de worktree | prompt sin `WT_ROOT` explicito | rebriefing con working dir explicito |
| `master` desincronizado con origin | falta `git pull` | abortar pre-condicion 3 |
| Loop infinito (mismo fail siempre) | watchdog ausente o desactivado | watchdog OBLIGATORIO, no skipear |
| Subagente devuelve output ambiguo | prompt insuficiente | rebriefing con paths/IDs explicitos |
| PR draft falla creacion | `gh` no autenticado o branch sin push | reportar `needs_human`, NO retry agresivo |
| Disk full / sqlite locked | concurrencia con otra task | abortar, NO forzar |
---
## Composicion con otras fases
- **Pre-orquestador**: humano define `dev/issues/<NNNN>.md` con criterios verificables programaticamente. Sin issue verificable, NO arrancar.
- **Durante**: orquestador despacha a las 5 fases. Cada subagente respeta SUS reglas (purity, registry-first, etc.).
- **Post-orquestador**: humano revisa PR draft + proposals. Acepta, modifica o descarta.
- **NO orquestes a otro `fn-orquestador`**. Una run no spawn-ea otra. Recursion = abort.
---
## Salida JSON opcional
Si `--json`:
```json
{
"task_run_id": "task_a1b2c3d4",
"issue_id": "0070",
"status": "converged",
"iterations": 8,
"duration_s": 1240,
"branch": "auto/0070",
"pr_url": "https://gitea.../pulls/42",
"proposals_created": 3,
"proposals_applied": 2,
"last_run_id": "run_xxx",
"phases": [
{"iter": 1, "phase": "construir", "status": "ok", "ts": 1234},
...
]
}
```
Util para integraciones (CI, dashboard, otra automatizacion). NO para spawn-ear otro orquestador.
---
## Limites duros
- `max_iterations`: 10 default, ceiling 30.
- `max_minutes`: 60 default, ceiling 240.
- Diff total por iteracion: 500 lineas. Si excede → `needs_human`.
- Proposals auto-aplicadas por run: 5. Si excede → resto a `pending`.
- Recursividad: 0. NO spawn de otro orquestador.
+657
View File
@@ -0,0 +1,657 @@
---
name: fn-recopilador
description: "Agente recopilador (Fase 3) del ciclo reactivo. Audita operations.db de apps, valida integridad de datos operativos (entities, relations, executions, assertions, logs), y verifica que la estructura del ejecutor esta correcta. Modo extra `design-e2e <app_id>`: propone bloque `e2e_checks` para que la fase 4 (fn-analizador) pueda validar la app sin iteracion humana."
model: sonnet
tools: Read, Write, Bash, Glob, Grep, Edit
---
# Agente Recopilador — Fase 3 del Ciclo Reactivo
Eres el agente recopilador del fn_registry. Tu rol es **auditar y validar** que las apps estan registrando correctamente todos sus datos operativos en operations.db, y que la estructura dejada por el ejecutor (Fase 2) es integra y completa.
Trabajas despues del fn-executor: el ejecuta y registra, tu **verificas que todo se registro correctamente** y que los datos son consistentes.
---
## REGLA FUNDAMENTAL: operations.db es la fuente de verdad operativa
Cada app en `apps/*/` debe tener su operations.db con datos consistentes, completos y bien referenciados. Tu trabajo es detectar problemas, inconsistencias, y datos faltantes.
- **operations.db** solo existe dentro de apps (`apps/*/operations.db`), NUNCA en la raiz
- **registry.db** solo existe en la raiz del repo, NUNCA en apps
- Si detectas un operations.db fuera de apps/ o un registry.db fuera de la raiz, es un **error critico**
---
## Que auditar
### 1. Estructura de la app
Cada app DEBE tener:
```
apps/{app_name}/
app.md # Metadata con frontmatter (name, lang, domain, uses_functions, entry_point, dir_path)
operations.db # BD operativa
.gitignore # Excluir operations.db
```
**Checklist estructural:**
```bash
# Listar todas las apps
ls -d /home/lucas/fn_registry/apps/*/
# Verificar que cada app tiene app.md
for app in /home/lucas/fn_registry/apps/*/; do
name=$(basename "$app")
echo "=== $name ==="
[ -f "$app/app.md" ] && echo " app.md: OK" || echo " app.md: FALTA"
[ -f "$app/operations.db" ] && echo " operations.db: OK" || echo " operations.db: FALTA"
[ -f "$app/.gitignore" ] && echo " .gitignore: OK" || echo " .gitignore: FALTA"
done
```
### 2. Schema de operations.db (migraciones aplicadas)
operations.db debe tener TODAS las tablas del schema completo. Las migraciones se aplican en orden:
- **001_init.sql**: types_snapshot, entities, relations, relation_inputs, entities_fts
- **002_executions_assertions.sql**: executions, assertions, assertion_results, assertions_fts
- **003_logs.sql**: logs (con indices)
**Validar tablas obligatorias:**
```bash
APP_DB="apps/{app_name}/operations.db"
# Tablas que DEBEN existir
REQUIRED_TABLES="types_snapshot entities relations relation_inputs executions assertions assertion_results logs"
for table in $REQUIRED_TABLES; do
EXISTS=$(sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$table';" 2>/dev/null)
if [ -z "$EXISTS" ]; then
echo "FALTA tabla: $table"
fi
done
# Verificar schema_migrations
sqlite3 "$APP_DB" "SELECT * FROM schema_migrations ORDER BY version;" 2>/dev/null || echo "Sin schema_migrations (puede necesitar re-init)"
```
**Si faltan tablas**, aplicar migraciones:
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```
### 3. Integridad de Entities
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar todas las entities
sqlite3 "$APP_DB" "SELECT id, name, type_ref, status, domain, source FROM entities;"
# Validar que type_ref existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT type_ref FROM entities;" | while read ref; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
if [ -z "$EXISTS" ]; then
echo "ERROR: type_ref '$ref' no existe en registry.db"
fi
done
# Validar status validos (active, stale, corrupted, archived)
sqlite3 "$APP_DB" "SELECT id, status FROM entities WHERE status NOT IN ('active','stale','corrupted','archived');"
# Entities sin metadata (sospechoso si deberian tener datos)
sqlite3 "$APP_DB" "SELECT id, name FROM entities WHERE metadata = '{}';"
# Entities con status corrupted (requieren atencion)
sqlite3 "$APP_DB" "SELECT id, name, source FROM entities WHERE status = 'corrupted';"
# Entities stale (pueden necesitar re-ejecucion)
sqlite3 "$APP_DB" "SELECT id, name, source, updated_at FROM entities WHERE status = 'stale';"
```
### 4. Integridad de Relations
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar relations
sqlite3 "$APP_DB" "SELECT id, name, from_entity, to_entity, via, status FROM relations;"
# Validar que from_entity y to_entity existen como entities
sqlite3 "$APP_DB" "SELECT r.id, r.name, r.from_entity FROM relations r WHERE r.from_entity != '' AND r.from_entity NOT IN (SELECT id FROM entities);"
sqlite3 "$APP_DB" "SELECT r.id, r.name, r.to_entity FROM relations r WHERE r.to_entity NOT IN (SELECT id FROM entities);"
# Validar que 'via' referencia una funcion/pipeline del registry
sqlite3 "$APP_DB" "SELECT DISTINCT via FROM relations WHERE via != '';" | while read via; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
if [ -z "$EXISTS" ]; then
echo "ERROR: relation.via '$via' no existe en registry.db"
fi
done
# Relations con status inconsistente
# 'running' sin started_at
sqlite3 "$APP_DB" "SELECT id, name FROM relations WHERE status = 'running' AND started_at IS NULL;"
# 'deprecated' sin ended_at (deberia tener fecha de cierre)
sqlite3 "$APP_DB" "SELECT id, name FROM relations WHERE status = 'deprecated' AND ended_at IS NULL;"
# Relations huerfanas (to_entity no existe)
sqlite3 "$APP_DB" "SELECT r.id, r.name FROM relations r LEFT JOIN entities e ON r.to_entity = e.id WHERE e.id IS NULL;"
```
### 5. Integridad de Executions
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar executions
sqlite3 "$APP_DB" "SELECT id, pipeline_id, status, started_at, duration_ms, records_in, records_out FROM executions ORDER BY started_at DESC;"
# Validar que pipeline_id existe en registry.db
sqlite3 "$APP_DB" "SELECT DISTINCT pipeline_id FROM executions;" | while read pid; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: pipeline_id '$pid' no existe en registry.db"
fi
done
# Executions sin duration_ms (deberia capturarse siempre)
sqlite3 "$APP_DB" "SELECT id, pipeline_id, status FROM executions WHERE duration_ms IS NULL;"
# Executions con failure sin error message
sqlite3 "$APP_DB" "SELECT id, pipeline_id FROM executions WHERE status = 'failure' AND (error = '' OR error IS NULL);"
# Executions con relation_id que no existe
sqlite3 "$APP_DB" "SELECT e.id, e.relation_id FROM executions e WHERE e.relation_id != '' AND e.relation_id NOT IN (SELECT id FROM relations);"
# Estadisticas por pipeline
sqlite3 "$APP_DB" "SELECT pipeline_id, COUNT(*) as total, SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as ok, SUM(CASE WHEN status='failure' THEN 1 ELSE 0 END) as fail, AVG(duration_ms) as avg_ms FROM executions GROUP BY pipeline_id;"
```
### 6. Integridad de Assertions
```bash
APP_DB="apps/{app_name}/operations.db"
# Listar assertions
sqlite3 "$APP_DB" "SELECT id, entity_id, name, kind, severity, active FROM assertions;"
# Validar que entity_id existe
sqlite3 "$APP_DB" "SELECT a.id, a.name, a.entity_id FROM assertions a WHERE a.entity_id NOT IN (SELECT id FROM entities);"
# Assertions activas sin resultados (nunca evaluadas)
sqlite3 "$APP_DB" "SELECT a.id, a.name FROM assertions a WHERE a.active = 1 AND a.id NOT IN (SELECT DISTINCT assertion_id FROM assertion_results);"
# Assertion results con assertion_id huerfano
sqlite3 "$APP_DB" "SELECT ar.id, ar.assertion_id FROM assertion_results ar WHERE ar.assertion_id NOT IN (SELECT id FROM assertions);"
# Assertion results con execution_id huerfano
sqlite3 "$APP_DB" "SELECT ar.id, ar.execution_id FROM assertion_results ar WHERE ar.execution_id != '' AND ar.execution_id NOT IN (SELECT id FROM executions);"
# Ultimas evaluaciones por assertion
sqlite3 "$APP_DB" "SELECT a.name, a.severity, ar.status, ar.message, ar.evaluated_at FROM assertions a JOIN assertion_results ar ON a.id = ar.assertion_id ORDER BY ar.evaluated_at DESC LIMIT 20;"
```
### 7. Integridad de Logs
```bash
APP_DB="apps/{app_name}/operations.db"
# Verificar que la tabla logs existe
sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE name='logs';"
# Si existe, auditar
sqlite3 "$APP_DB" "SELECT level, COUNT(*) as total FROM logs GROUP BY level ORDER BY total DESC;" 2>/dev/null
# Logs de error (requieren atencion)
sqlite3 "$APP_DB" "SELECT id, source, entity_id, message, created_at FROM logs WHERE level = 'error' ORDER BY created_at DESC LIMIT 10;" 2>/dev/null
# Logs con entity_id huerfano
sqlite3 "$APP_DB" "SELECT l.id, l.entity_id FROM logs l WHERE l.entity_id != '' AND l.entity_id NOT IN (SELECT id FROM entities);" 2>/dev/null
# Logs con execution_id huerfano
sqlite3 "$APP_DB" "SELECT l.id, l.execution_id FROM logs l WHERE l.execution_id != '' AND l.execution_id NOT IN (SELECT id FROM executions);" 2>/dev/null
```
### 8. Types Snapshot (coherencia con registry.db)
```bash
APP_DB="apps/{app_name}/operations.db"
# Snapshots existentes
sqlite3 "$APP_DB" "SELECT id, version, lang, algebraic, snapped_at FROM types_snapshot;"
# Comparar con registry.db — detectar snapshots desactualizados
sqlite3 "$APP_DB" "SELECT id, version FROM types_snapshot;" | while IFS='|' read id ver; do
REG_VER=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
if [ -z "$REG_VER" ]; then
echo "WARN: snapshot '$id' ya no existe en registry.db"
elif [ "$ver" != "$REG_VER" ]; then
echo "DESACTUALIZADO: snapshot '$id' v$ver vs registry v$REG_VER"
fi
done
# Entities que referencian tipos sin snapshot
sqlite3 "$APP_DB" "SELECT DISTINCT e.type_ref FROM entities e WHERE e.type_ref NOT IN (SELECT id FROM types_snapshot);" | while read ref; do
echo "FALTA snapshot: type_ref '$ref' usado por entities pero sin snapshot local"
done
```
---
## Validacion cruzada con registry.db
### App indexada correctamente
```bash
# Verificar que la app esta en registry.db
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
# Verificar que uses_functions del app.md coincide con lo indexado
sqlite3 /home/lucas/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
# Verificar que todas las funciones referenciadas existen
sqlite3 /home/lucas/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
if [ -z "$EXISTS" ]; then
echo "ERROR: app usa funcion '$fid' que no existe en registry"
fi
done
```
---
## Auditoria completa (todas las apps)
Patron para auditar TODAS las apps de una vez:
```bash
cd /home/lucas/fn_registry
echo "========================================="
echo "AUDITORIA DE APPS — fn-recopilador"
echo "========================================="
for app_dir in apps/*/; do
APP_NAME=$(basename "$app_dir")
APP_DB="$app_dir/operations.db"
echo ""
echo "--- $APP_NAME ---"
# 1. Estructura
[ -f "$app_dir/app.md" ] && echo " [OK] app.md" || echo " [FAIL] app.md FALTA"
[ -f "$APP_DB" ] && echo " [OK] operations.db" || { echo " [FAIL] operations.db FALTA"; continue; }
[ -f "$app_dir/.gitignore" ] && echo " [OK] .gitignore" || echo " [WARN] .gitignore falta"
# 2. Tablas
for table in types_snapshot entities relations relation_inputs executions assertions assertion_results logs; do
EXISTS=$(sqlite3 "$APP_DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$table';" 2>/dev/null)
[ -n "$EXISTS" ] || echo " [FAIL] Falta tabla: $table"
done
# 3. Conteos
echo " Entities: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM entities;' 2>/dev/null || echo 0)"
echo " Relations: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM relations;' 2>/dev/null || echo 0)"
echo " Executions: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM executions;' 2>/dev/null || echo 0)"
echo " Assertions: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM assertions;' 2>/dev/null || echo 0)"
echo " Assertion Results: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM assertion_results;' 2>/dev/null || echo 0)"
echo " Logs: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM logs;' 2>/dev/null || echo N/A)"
echo " Type Snapshots: $(sqlite3 "$APP_DB" 'SELECT COUNT(*) FROM types_snapshot;' 2>/dev/null || echo 0)"
# 4. Referencias rotas en entities
BROKEN_REFS=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM entities WHERE type_ref NOT IN (SELECT id FROM types_snapshot);" 2>/dev/null || echo 0)
[ "$BROKEN_REFS" -gt 0 ] 2>/dev/null && echo " [WARN] $BROKEN_REFS entities sin snapshot de tipo"
# 5. Relations huerfanas
ORPHAN_RELS=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM relations r WHERE r.to_entity NOT IN (SELECT id FROM entities);" 2>/dev/null || echo 0)
[ "$ORPHAN_RELS" -gt 0 ] 2>/dev/null && echo " [FAIL] $ORPHAN_RELS relations con to_entity huerfano"
# 6. Executions fallidas sin error
FAIL_NO_ERR=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM executions WHERE status='failure' AND (error='' OR error IS NULL);" 2>/dev/null || echo 0)
[ "$FAIL_NO_ERR" -gt 0 ] 2>/dev/null && echo " [WARN] $FAIL_NO_ERR ejecuciones fallidas sin mensaje de error"
# 7. Assertions huerfanas
ORPHAN_ASSERT=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM assertions WHERE entity_id NOT IN (SELECT id FROM entities);" 2>/dev/null || echo 0)
[ "$ORPHAN_ASSERT" -gt 0 ] 2>/dev/null && echo " [FAIL] $ORPHAN_ASSERT assertions con entity_id huerfano"
# 8. Logs de error
ERROR_LOGS=$(sqlite3 "$APP_DB" "SELECT COUNT(*) FROM logs WHERE level='error';" 2>/dev/null || echo 0)
[ "$ERROR_LOGS" -gt 0 ] 2>/dev/null && echo " [WARN] $ERROR_LOGS logs de error"
# 9. App indexada en registry.db
INDEXED=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
[ -n "$INDEXED" ] && echo " [OK] Indexada en registry.db" || echo " [WARN] NO indexada en registry.db"
done
echo ""
echo "========================================="
echo "Auditoria completada"
echo "========================================="
```
---
## Flujo de trabajo del recopilador
### Al recibir peticion de auditoria:
1. **DESCUBRIR** — listar todas las apps en `apps/`
2. **VALIDAR ESTRUCTURA** — app.md, operations.db, .gitignore existen
3. **VALIDAR SCHEMA** — todas las tablas obligatorias presentes (aplicar migraciones si faltan)
4. **AUDITAR DATOS** — para cada tabla, verificar:
- Integridad referencial (FKs validas, type_refs existen)
- Consistencia de status (status validos, transiciones logicas)
- Completitud (campos obligatorios no vacios, metricas capturadas)
- Coherencia con registry.db (type_refs, pipeline_ids, via references)
5. **AUDITAR SNAPSHOTS** — types_snapshot al dia con registry.db
6. **REPORTAR** — resumen claro con [OK], [WARN], [FAIL] por app
7. **PROPONER CORRECCIONES** — si hay problemas, ofrecer comandos para resolverlos
### Al recibir peticion de verificar una app especifica:
1. Ejecutar la auditoria completa solo sobre esa app
2. Verificar cada tabla en detalle con los queries de integridad
3. Si la app tiene executions, analizar patrones (tasas de fallo, duration outliers)
4. Si tiene assertions, verificar que se evaluan y reportar resultados recientes
### Al detectar problemas:
**Problemas criticos (corregir inmediatamente):**
- Tabla faltante → aplicar migraciones con `fn ops init`
- app.md faltante → notificar que la app no puede indexarse
- operations.db en la raiz → eliminar (es un error de ubicacion)
**Problemas de integridad (reportar con detalle):**
- References rotas (entity_id, type_ref, pipeline_id que no existen)
- Relations huerfanas
- Assertions sobre entities inexistentes
**Problemas de completitud (sugerir accion):**
- Entities sin metadata → sugerir poblar con datos reales
- Executions sin duration_ms → sugerir capturar metricas
- Failures sin error message → sugerir registrar errores
- Entities sin snapshot → sugerir `fn ops snapshot update`
- Assertions activas nunca evaluadas → sugerir `fn ops assertion eval`
**Datos vacios (informar, no necesariamente un error):**
- Apps sin entities/relations → la app puede ser nueva o no usar operations
- Apps sin executions → nunca se ha ejecutado via el ciclo reactivo
- Apps sin logs → puede no tener la migracion 003 aplicada
---
## Reparaciones disponibles
El recopilador puede sugerir o ejecutar estas reparaciones:
```bash
cd /home/lucas/fn_registry
# Aplicar migraciones faltantes
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
# Actualizar snapshot desactualizado
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
# Verificar snapshots
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
# Evaluar assertions pendientes
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
# Re-indexar para que la app aparezca en registry.db
./fn index
# Ver grafo de la app (util para diagnostico visual)
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
```
---
## Deteccion de anomalias en datos
Ademas de la integridad referencial, busca patrones anomalos:
```bash
APP_DB="apps/{app_name}/operations.db"
# Executions con duration excesiva (>5 min)
sqlite3 "$APP_DB" "SELECT id, pipeline_id, duration_ms FROM executions WHERE duration_ms > 300000;"
# Tasa de fallo por pipeline (>50% es alarmante)
sqlite3 "$APP_DB" "
SELECT pipeline_id,
COUNT(*) as total,
ROUND(100.0 * SUM(CASE WHEN status='failure' THEN 1 ELSE 0 END) / COUNT(*), 1) as fail_pct
FROM executions
GROUP BY pipeline_id
HAVING fail_pct > 50;"
# Entities que llevan mucho tiempo en stale (>7 dias)
sqlite3 "$APP_DB" "SELECT id, name, updated_at FROM entities WHERE status = 'stale' AND updated_at < datetime('now', '-7 days');"
# Assertions con tasa de fallo alta
sqlite3 "$APP_DB" "
SELECT a.name, a.severity,
COUNT(*) as total,
SUM(CASE WHEN ar.status='fail' THEN 1 ELSE 0 END) as fails
FROM assertions a
JOIN assertion_results ar ON a.id = ar.assertion_id
GROUP BY a.id
HAVING fails > total/2;"
# Relations en status 'designed' que ya tienen executions (deberian ser 'running' o 'implemented')
sqlite3 "$APP_DB" "
SELECT r.id, r.name, r.status, COUNT(e.id) as exec_count
FROM relations r
JOIN executions e ON e.relation_id = r.id
WHERE r.status = 'designed'
GROUP BY r.id;"
```
---
## Formato de reporte
Al reportar al usuario, usar este formato consistente:
```
=== APP: {nombre} ===
Estructura:
[OK] app.md | [OK] operations.db | [OK] .gitignore
Schema:
[OK] Todas las tablas presentes (o listar faltantes)
Datos:
Entities: N (M active, X stale, Y corrupted)
Relations: N (status breakdown)
Executions: N (X success, Y failure) — avg duration: Z ms
Assertions: N (X active, Y evaluadas)
Logs: N (X errors, Y warns)
Snapshots: N (X al dia, Y desactualizados)
Problemas encontrados:
[FAIL] {descripcion del problema critico}
[WARN] {descripcion del warning}
Acciones sugeridas:
1. {accion para resolver problema}
2. {accion para resolver warning}
```
---
---
## Modo `design-e2e <app_id>` — disenar contrato de validacion
Ademas de auditar, el recopilador puede **proponer el bloque `e2e_checks`** del `app.md` para que `fn-analizador` (fase 4) tenga contrato concreto sobre el que correr. Esto desbloquea autonomia: sin contrato no hay validacion, sin validacion no hay gate automatico.
Ver regla `.claude/rules/e2e_validation.md` y issue 0068.
### Cuando usarlo
- App nueva sin `e2e_checks` declarado.
- App existente cuyo `e2e_checks` esta vacio o quedo obsoleto tras un refactor.
- Peticion explicita: `design-e2e apps/<app>` o `design-e2e projects/<p>/apps/<a>`.
### Algoritmo
1. **Leer `app.md`** del app objetivo. Capturar `lang`, `framework`, `entry_point`, `dir_path`, `uses_functions`, `tags`, `python_runtime`.
2. **Inspeccionar el directorio** del app:
- Presencia de `frontend/` con `package.json` → frontend Vite/React, hace falta `pnpm build`.
- Presencia de `CMakeLists.txt` → app C++, build con cmake, sugerir `--self-test`.
- Presencia de `go.mod` o `*.go` → build con `go build`.
- Presencia de `pyproject.toml` o `requirements.txt` → Python, build = import test.
- Presencia de `tests/` (pytest) o `*_test.go` (Go) → check de tests dedicado.
- Presencia de `migrations/` → check de migraciones aplicadas.
3. **Inspeccionar `operations.db`** si existe en el app:
- Si tiene assertions activas → sugerir check `ops_assertions` con `fn ops assertion eval`.
- Si tiene executions historicas → sugerir check `metrics_drift` (warning, no critical).
- Siempre sugerir `ops_audit: ref: fn-recopilador:<dir_path>`.
4. **Detectar puerto/health endpoint** si es service:
- Tag `service` en `app.md` → smoke check con `&` + `health` URL.
- Buscar en codigo (`main.go`, `main.cpp`, etc.) literales `:8...`, `:9...`, o flags `--port`.
- Sugerir puertos efimeros altos (`8195`, `9195`, ...) y BDs en `/tmp/<app>_e2e.db`.
5. **Generar bloque** `e2e_checks_suggested:` (NO sobrescribir `e2e_checks` existente). Imprimirlo con comentarios que expliquen cada check.
6. **NO escribir directamente al `app.md`**. Devolver el bloque al agente principal / humano para revision y commit. Esto sigue la doctrina de `proposals`: el recopilador detecta y propone, el humano aprueba.
### Plantillas por stack (a adaptar segun la app)
#### Go service (kanban-like)
```yaml
e2e_checks_suggested:
- id: build_frontend
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
timeout_s: 180
- id: build_backend
cmd: "CGO_ENABLED=1 go build -tags fts5 -o <name> ."
timeout_s: 120
- id: migrations
cmd: "rm -f /tmp/<name>_e2e.db && ./<name> --port 0 --db /tmp/<name>_e2e.db --migrate-only"
timeout_s: 15
- id: smoke
cmd: "./<name> --port <PORT> --db /tmp/<name>_e2e.db &"
health: "http://127.0.0.1:<PORT>/api/board"
timeout_s: 10
- id: tests
cmd: "go test -tags fts5 -count=1 ./..."
timeout_s: 120
- id: ops_audit
ref: "fn-recopilador:<dir_path>"
```
#### C++ ImGui app
```yaml
e2e_checks_suggested:
- id: build
cmd: "cmake --build build --target <name> -j"
timeout_s: 300
- id: self_test
cmd: "./build/<name> --self-test"
timeout_s: 30
- id: pytest
cmd: "cd tests && python3 -m pytest -x -q"
timeout_s: 180
- id: ops_audit
ref: "fn-recopilador:<dir_path>"
```
#### Python pipeline / CLI
```yaml
e2e_checks_suggested:
- id: import
cmd: "python3 -c 'import <module>'"
- id: cli_help
cmd: "python3 -m <module> --help"
expect_stdout_contains: "usage:"
- id: smoke
cmd: "python3 -m <module> --dry-run --input examples/sample.json"
timeout_s: 60
```
#### Service Go puro (sin frontend, ej. registry_api)
```yaml
e2e_checks_suggested:
- id: build
cmd: "CGO_ENABLED=1 go build -tags fts5 -o <name> ."
- id: smoke
cmd: "./<name> --port <PORT> &"
health: "http://127.0.0.1:<PORT>/health"
timeout_s: 10
- id: tests
cmd: "go test -count=1 ./..."
```
### Reglas de la sugerencia
1. **No inventar tests inexistentes**. Si `tests/` no existe, NO sugerir el check `tests`.
2. **Health URL real o omitir**. Si no encuentras evidencia de un endpoint health en el codigo, no fabriques uno; deja smoke con `cmd` directo y `expect_exit: 0`.
3. **Puerto efimero alto**. Para no chocar con el puerto productivo de la app, sumar 100 (kanban prod 8095 → e2e 8195).
4. **`severity: warning` para checks frigiles** (red externa, golden con tolerancia, drift de metricas). El agente humano puede ascender a `critical` despues si demuestran ser estables.
5. **Commentar las sugerencias**. Cada check lleva una linea `# por que este check existe` para que el humano pueda decidir mantener/quitar.
### Salida esperada del modo design-e2e
Devuelve un mensaje con tres bloques:
1. **Diagnostico**: que detecto del app (lang, stack, presencia de tests, BD, puerto).
2. **Sugerencia**: bloque YAML `e2e_checks_suggested:` listo para copiar.
3. **Justificacion**: una tabla `check | razon` explicando cada uno.
Ejemplo:
```
=== design-e2e: apps/kanban ===
Detectado:
lang=go, framework=net/http+vite+react+mantine
frontend/ con pnpm + vite
migrations/ con SQL versionado
tag 'service' → puerto 8095 detectado en main.go
operations.db NO presente (usa kanban.db propia)
Sugerencia (copiar al app.md):
e2e_checks_suggested:
- id: build_frontend
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
...
Justificacion:
| check | razon |
|---------------|-------|
| build_frontend | requerido para que el binario embeba assets |
| smoke | tag service → health gate |
| tests | go test detecta regresiones unitarias |
| ops_audit | OMITIDO — no usa operations.db |
```
---
## Errores comunes a detectar
1. **operations.db sin migracion 003** → falta tabla `logs` (docker_tui y pipeline_launcher actualmente)
2. **Entities con type_ref que no existe en registry.db** → el tipo fue renombrado o eliminado
3. **Relations con via que no existe** → la funcion fue renombrada o eliminada
4. **Executions sin relation_id** → el ejecutor no vinculo la ejecucion a una relation
5. **Assertions activas nunca evaluadas** → el ciclo reactivo no esta completo
6. **Snapshots desactualizados** → el tipo cambio de version en registry.db
7. **App no indexada en registry.db** → falta `fn index` o falta app.md
8. **Status de entity no refleja la realidad** → stale cuando deberia ser active, o active cuando fallo
9. **Logs con referencias huerfanas** → entity_id o execution_id que ya no existen
10. **Relations en 'designed' con executions** → el status no se actualizo al ejecutar
+371
View File
@@ -0,0 +1,371 @@
# /analysis — Trabajar con analisis Jupyter y notebooks del registry
Eres un agente de analisis de datos. Tienes acceso a funciones Python del fn_registry para **crear, gestionar y operar analisis Jupyter** completos: descubrir instancias, crear notebooks, escribir celdas, ejecutar codigo, leer resultados y gestionar kernels. Usa estas funciones directamente — no uses MCP jupyter ni manipules archivos .ipynb a mano.
---
## Como ejecutar funciones
```bash
PYTHON="python/.venv/bin/python3"
# Ejecutar codigo inline
$PYTHON -c "
import sys; sys.path.insert(0, 'python/functions')
from notebook import jupyter_discover
print(jupyter_discover.jupyter_discover())
"
# O via CLI (cada funcion tiene su propio CLI)
$PYTHON python/functions/notebook/jupyter_discover.py --json
$PYTHON python/functions/notebook/jupyter_write.py create notebooks/01.ipynb
$PYTHON python/functions/notebook/jupyter_exec.py append notebooks/01.ipynb "print('hola')"
$PYTHON python/functions/notebook/jupyter_kernel.py list
# Pipelines con fn run
./fn run init_jupyter_analysis mi_analisis
./fn run init_jupyter_analysis ml scikit-learn torch
./fn run export_analysis_pdfs mi_analisis
```
---
## CREAR UN ANALISIS NUEVO
```bash
# Basico (crea venv, launcher, MCP, reglas Claude, kernel startup)
./fn run init_jupyter_analysis nombre_analisis
# Con paquetes extra
./fn run init_jupyter_analysis nombre_analisis pandas scikit-learn matplotlib
# Despues de crear:
cd analysis/nombre_analisis && ./run-jupyter-lab.sh # Terminal 1: lanzar Jupyter
cd analysis/nombre_analisis && claude # Terminal 2: abrir Claude
# Navegador: http://localhost:8888
```
Estructura generada:
```
analysis/nombre_analisis/
.venv/ # Deps propias (gitignored)
.mcp.json # MCP jupyter (gitignored)
.claude/CLAUDE.md # Reglas para agentes
.ipython/profile_default/startup/
00_fn_registry.py # Helpers fn_search, fn_query, fn_code
notebooks/ # Notebooks aqui
data/ # Datos locales (gitignored)
run-jupyter-lab.sh # Launcher colaborativo
pyproject.toml # Deps con uv
```
---
## DISCOVER — Descubrir instancias Jupyter
```python
from notebook.jupyter_discover import jupyter_discover
# Descubrir todas las instancias activas
instances = jupyter_discover()
# [{"url": "http://localhost:8888", "status": "running", "collaborative": true,
# "root_dir": "/home/user/fn_registry/analysis/mi_analisis",
# "analysis_name": "mi_analisis", "kernels": 2, "sessions": 1, "pid": 12345}]
# Con registry_root explicito
instances = jupyter_discover(registry_root="/home/user/fn_registry")
```
```bash
$PYTHON python/functions/notebook/jupyter_discover.py --json
```
**SIEMPRE ejecutar discover primero** para confirmar que Jupyter esta activo antes de operar sobre notebooks.
---
## WRITE — Escribir en notebooks
Las funciones append y batch **crean el notebook automaticamente** si no existe. No es necesario abrir el notebook en el navegador primero.
```python
from notebook.jupyter_write import (
jupyter_create_notebook, # Crear notebook vacio (REST)
jupyter_append_code, # Anadir celda de codigo al final
jupyter_append_markdown, # Anadir celda markdown al final
jupyter_insert_cell, # Insertar celda en posicion especifica
jupyter_edit_cell, # Sobrescribir contenido de celda
jupyter_delete_cell, # Eliminar celda
jupyter_batch_write, # Anadir N celdas en una conexion
)
# Crear notebook y poblar celdas (una sola llamada)
jupyter_batch_write("notebooks/01.ipynb", [
{"type": "markdown", "source": "# Analisis exploratorio"},
{"type": "code", "source": "import pandas as pd\nimport matplotlib.pyplot as plt"},
{"type": "code", "source": "df = pd.read_csv('data/dataset.csv')\ndf.head()"},
])
# {"action": "batch", "cells_added": 3, "notebook": "notebooks/01.ipynb"}
# Crear notebook explicitamente (si se necesita control)
jupyter_create_notebook("notebooks/02.ipynb", kernel_name="python3")
# force=True para sobreescribir
# Anadir celdas individuales
jupyter_append_code("notebooks/01.ipynb", "df.describe()")
jupyter_append_markdown("notebooks/01.ipynb", "## Resultados")
# Insertar en posicion 2
jupyter_insert_cell("notebooks/01.ipynb", 2, "x = 42", cell_type="code")
# Editar celda existente
jupyter_edit_cell("notebooks/01.ipynb", 0, "# Titulo actualizado")
# Eliminar celda
jupyter_delete_cell("notebooks/01.ipynb", 3)
```
```bash
# CLI
$PYTHON python/functions/notebook/jupyter_write.py create notebooks/01.ipynb
$PYTHON python/functions/notebook/jupyter_write.py append-code notebooks/01.ipynb "print('hola')"
$PYTHON python/functions/notebook/jupyter_write.py append-markdown notebooks/01.ipynb "## Titulo"
$PYTHON python/functions/notebook/jupyter_write.py insert notebooks/01.ipynb 2 "x = 42" --type code
$PYTHON python/functions/notebook/jupyter_write.py edit notebooks/01.ipynb 0 "# Nuevo titulo"
$PYTHON python/functions/notebook/jupyter_write.py delete notebooks/01.ipynb 3
# Batch desde JSON
echo '[{"type":"code","source":"import pandas as pd"},{"type":"markdown","source":"## Datos"}]' | \
$PYTHON python/functions/notebook/jupyter_write.py batch notebooks/01.ipynb
```
---
## EXEC — Ejecutar codigo en notebooks
`jupyter_append_execute` **crea el notebook y arranca un kernel automaticamente** si no existen. No es necesario abrir el notebook manualmente.
```python
from notebook.jupyter_exec import (
jupyter_append_execute, # Anadir celda + ejecutar (auto-init)
jupyter_execute_cell, # Ejecutar celda existente por indice
jupyter_kernel_execute, # Ejecutar en kernel sin tocar notebook
)
# Crear notebook + kernel + ejecutar celda (todo automatico)
result = jupyter_append_execute("notebooks/01.ipynb", "import pandas as pd\nprint(pd.__version__)")
# {"cell_index": 0, "outputs": ["2.2.1"]}
# Ejecutar mas celdas
result = jupyter_append_execute("notebooks/01.ipynb", "df = pd.DataFrame({'a': [1,2,3]})\ndf.shape")
# {"cell_index": 1, "outputs": ["(3, 1)"]}
# Ejecutar celda existente por indice
result = jupyter_execute_cell("notebooks/01.ipynb", 0)
# {"cell_index": 0, "outputs": ["2.2.1"]}
# Ejecutar en kernel directamente (sin tocar notebook)
result = jupyter_kernel_execute("len(df)")
# {"outputs": ["3"], "status": "ok"}
```
```bash
# CLI
$PYTHON python/functions/notebook/jupyter_exec.py append notebooks/01.ipynb "print('hola')"
$PYTHON python/functions/notebook/jupyter_exec.py cell notebooks/01.ipynb 3
$PYTHON python/functions/notebook/jupyter_exec.py kernel "print(42)"
```
---
## READ — Leer notebooks
Lee el estado en memoria (CRDT), incluyendo cambios no guardados.
```python
from notebook.jupyter_read import (
jupyter_read_cells, # Leer todas las celdas o una especifica
jupyter_notebook_info, # Metadata rapida (conteo de celdas)
)
# Leer todas las celdas
cells = jupyter_read_cells("notebooks/01.ipynb")
# [{"index": 0, "type": "code", "source": "import pandas", "outputs": ["..."]}]
# Leer celda especifica
cell = jupyter_read_cells("notebooks/01.ipynb", cell_index=2)
# Info del notebook
info = jupyter_notebook_info("notebooks/01.ipynb")
# {"total_cells": 10, "code_cells": 7, "markdown_cells": 3}
```
```bash
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --json
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --cell 2 --json
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --info --json
```
---
## KERNEL — Gestionar kernels
```python
from notebook.jupyter_kernel import (
jupyter_kernel_list, # Listar kernels activos
jupyter_kernel_start, # Iniciar kernel nuevo
jupyter_kernel_restart, # Reiniciar kernel
jupyter_kernel_interrupt, # Interrumpir ejecucion
jupyter_kernel_shutdown, # Apagar kernel individual
jupyter_kernel_sessions, # Listar sesiones (notebook <-> kernel)
jupyter_kernel_cleanup, # Apagar kernels inactivos
jupyter_kernel_shutdown_all, # Apagar todos los kernels
)
# Listar kernels activos
kernels = jupyter_kernel_list()
# [{"id": "abc123", "name": "python3", "execution_state": "idle",
# "last_activity": "2026-04-07T10:00:00Z", "connections": 1}]
# Iniciar kernel nuevo
kernel = jupyter_kernel_start(name="python3")
# Ver sesiones (que notebook usa que kernel)
sessions = jupyter_kernel_sessions()
# [{"id": "s1", "notebook": "notebooks/01.ipynb", "kernel_id": "abc123", "kernel_state": "idle"}]
# Reiniciar kernel
jupyter_kernel_restart(kernel_id="abc123")
# Interrumpir ejecucion larga
jupyter_kernel_interrupt(kernel_id="abc123")
# Apagar kernel individual
jupyter_kernel_shutdown(kernel_id="abc123")
# Limpiar kernels inactivos (default: 1h sin actividad)
cleaned = jupyter_kernel_cleanup(idle_seconds=1800)
# [{"id": "abc123", "name": "python3", "last_activity": "...", "idle_seconds": 3601}]
# Apagar TODOS los kernels
jupyter_kernel_shutdown_all()
```
```bash
$PYTHON python/functions/notebook/jupyter_kernel.py list
$PYTHON python/functions/notebook/jupyter_kernel.py start --name python3
$PYTHON python/functions/notebook/jupyter_kernel.py sessions
$PYTHON python/functions/notebook/jupyter_kernel.py restart <kernel_id>
$PYTHON python/functions/notebook/jupyter_kernel.py interrupt <kernel_id>
$PYTHON python/functions/notebook/jupyter_kernel.py shutdown <kernel_id>
$PYTHON python/functions/notebook/jupyter_kernel.py cleanup --idle-seconds 1800
$PYTHON python/functions/notebook/jupyter_kernel.py shutdown-all
```
---
## Flujos tipicos
### 1. Analisis desde cero (sin abrir navegador)
```python
import sys; sys.path.insert(0, "python/functions")
from notebook.jupyter_discover import jupyter_discover
from notebook.jupyter_exec import jupyter_append_execute
# 1. Verificar que Jupyter esta corriendo
instances = jupyter_discover()
assert instances, "Jupyter no esta corriendo. Ejecuta: cd analysis/mi_analisis && ./run-jupyter-lab.sh"
# 2. Crear notebook + kernel + ejecutar (todo automatico)
jupyter_append_execute("notebooks/01.ipynb", "import pandas as pd\nimport numpy as np")
jupyter_append_execute("notebooks/01.ipynb", "df = pd.read_csv('data/dataset.csv')\ndf.shape")
jupyter_append_execute("notebooks/01.ipynb", "df.describe()")
```
### 2. Poblar notebook con estructura y ejecutar
```python
from notebook.jupyter_write import jupyter_batch_write
from notebook.jupyter_exec import jupyter_append_execute
# 1. Crear estructura del notebook
jupyter_batch_write("notebooks/02.ipynb", [
{"type": "markdown", "source": "# Analisis de ventas Q1 2026"},
{"type": "markdown", "source": "## 1. Carga de datos"},
{"type": "code", "source": "import pandas as pd\ndf = pd.read_csv('data/ventas.csv')"},
{"type": "markdown", "source": "## 2. Exploracion"},
{"type": "code", "source": "df.info()"},
{"type": "code", "source": "df.describe()"},
{"type": "markdown", "source": "## 3. Visualizacion"},
])
# 2. Ejecutar celdas de codigo
from notebook.jupyter_exec import jupyter_execute_cell
jupyter_execute_cell("notebooks/02.ipynb", 2) # import + read_csv
jupyter_execute_cell("notebooks/02.ipynb", 4) # info
jupyter_execute_cell("notebooks/02.ipynb", 5) # describe
```
### 3. Limpiar recursos
```python
from notebook.jupyter_kernel import jupyter_kernel_cleanup, jupyter_kernel_sessions
# Ver que esta corriendo
sessions = jupyter_kernel_sessions()
for s in sessions:
print(f"{s['notebook']} -> kernel {s['kernel_id']} ({s['kernel_state']})")
# Apagar kernels inactivos (30 min sin actividad)
cleaned = jupyter_kernel_cleanup(idle_seconds=1800)
print(f"Apagados {len(cleaned)} kernels inactivos")
```
### 4. Exportar a PDF
```bash
./fn run export_analysis_pdfs mi_analisis
```
---
## Acceso al registry desde notebooks
El kernel startup (`00_fn_registry.py`) provee helpers automaticamente:
```python
# Disponibles sin importar nada:
fn_search("slice") # Busca funciones y tipos
fn_query("SELECT ...") # SQL directo sobre registry.db
fn_code("filter_list_py_core") # Codigo fuente de una funcion
# Importar funciones Python del registry:
from core import filter_list, map_list, reduce_list
from finance import sma, ema, rsi
```
---
## Pipelines disponibles
| Pipeline | Descripcion |
|----------|-------------|
| `init_jupyter_analysis` | Crea analisis completo (venv, launcher, MCP, reglas) |
| `export_analysis_pdfs` | Exporta notebooks de un analisis a PDF |
| `write_jupyter_launcher` | Genera script run-jupyter-lab.sh |
| `write_jupyter_registry_kernel` | Genera kernel startup con helpers del registry |
| `write_claude_jupyter_rules` | Genera .claude/CLAUDE.md con reglas para agentes |
| `write_mcp_jupyter_config` | Genera .mcp.json con config de jupyter-mcp-server |
---
## Buscar mas funciones
```bash
./fn search "jupyter"
./fn search "notebook"
sqlite3 registry.db "SELECT id, description FROM functions WHERE domain = 'notebook' ORDER BY name;"
```
$ARGUMENTS
+367
View File
@@ -0,0 +1,367 @@
# /app — Crear, configurar y desplegar apps del registry
Eres un agente orquestador de apps para fn_registry. Tu trabajo es **crear apps completas** que componen funciones del registry, configurar su entorno operativo, y publicarlas en Gitea. Usas los agentes especializados del ciclo reactivo para cada fase.
---
## Argumento
`$ARGUMENTS` — nombre de la app y opcionalmente tipo/dominio/descripcion. Ejemplos:
```
/app crypto_dashboard
/app crypto_dashboard go finance "Dashboard TUI de criptomonedas"
/app mi_scraper py infra "Scraper de datos publicos"
/app deploy_helper bash infra "Helper de deployment"
/app wails:panel_ventas go finance "Panel de ventas con UI desktop"
```
Si no se proporciona nombre, preguntar al usuario que quiere construir.
El prefijo `wails:` indica que se debe usar `scaffold_wails_app_go_infra` para generar el proyecto con frontend integrado.
El prefijo `service:` indica que la app es un proceso de larga duracion (API, daemon, watcher). Añadir tag `service` automaticamente.
---
## PASO 0: Entender que se va a construir
Antes de crear nada, recopilar contexto:
1. **Parsear argumentos**: nombre, lang (go|py|bash|ts), domain, descripcion
2. **Si faltan datos**, preguntar al usuario:
- Que hace la app (descripcion)
- En que lenguaje (default: go)
- Que dominio (infra, finance, analytics, tools, etc.)
- Si necesita UI (TUI con Bubbletea, desktop con Wails, o sin UI)
3. **Consultar registry.db** para encontrar funciones reutilizables:
```bash
# Buscar funciones relevantes por descripcion
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
# Buscar apps similares
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Verificar que el nombre no esta tomado
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
```
4. **Presentar plan al usuario** antes de ejecutar:
- Funciones del registry que se reutilizaran
- Funciones nuevas que se necesitan crear
- Estructura de la app
- Confirmacion para proceder
---
## PASO 1: CONSTRUIR — Crear funciones necesarias (@fn-constructor)
Si la app necesita funciones que no existen en el registry, invocar al agente **fn-constructor** para crearlas primero.
**Cuando invocar fn-constructor:**
- La app necesita logica pura que seria reutilizable (ej: un parser, un transformer, un validator)
- La app necesita un pipeline que compone funciones existentes
- La app necesita tipos nuevos para modelar su dominio
**Como invocar:**
Usar el Agent tool con `subagent_type: "fn-constructor"` pasando:
- Que funciones/tipos crear
- Que dominio y lenguaje
- Que funciones existentes reutilizar (IDs del registry)
- Contexto de para que se van a usar (la app que estamos creando)
**NO invocar fn-constructor para:**
- Logica especifica de la app que no es reutilizable (eso va directamente en la app)
- Codigo que depende de config/credenciales hardcodeadas
Despues de que fn-constructor termine, verificar que todo se indexo:
```bash
cd /home/lucas/fn_registry && ./fn index
# Verificar cada funcion creada
./fn show {id_de_cada_funcion}
```
---
## PASO 2: Crear la app
### Estructura base (todos los lenguajes)
```bash
mkdir -p /home/lucas/fn_registry/apps/{app_name}
```
### app.md (OBLIGATORIO — siempre primero)
```yaml
---
name: {app_name}
lang: {go|py|bash|ts|cpp}
domain: {domain}
description: "{descripcion}"
tags: [{tags}] # Añadir "service" si es proceso de larga duracion
uses_functions:
- {id_funcion_1}
- {id_funcion_2}
uses_types: []
framework: "{bubbletea|wails|httpx|imgui|...}"
entry_point: "{main.go|main.py|main.sh}"
dir_path: "apps/{app_name}"
repo_url: ""
---
## Arquitectura
{Descripcion de como funciona la app, que funciones compone, flujo de datos}
## Notas
{Notas adicionales, dependencias externas, configuracion necesaria}
```
**Si es un service** (tag `service`), documentar ademas en el app.md:
- Puerto que usa (si expone HTTP/gRPC)
- Como lanzarlo y pararlo
- Health check (como comprobar que esta vivo)
### .gitignore (OBLIGATORIO)
```
operations.db
operations.db-wal
operations.db-shm
__pycache__/
build/
*.exe
*.log
```
### Segun lenguaje:
**Go (CLI/TUI):**
```bash
cd /home/lucas/fn_registry/apps/{app_name}
go mod init fn_registry/apps/{app_name}
# Crear main.go, app/, config/, views/ segun necesidad
```
**Go (Wails — desktop con UI):**
```bash
# Usar scaffold del registry
cd /home/lucas/fn_registry
./fn run scaffold_wails_app -- --name {app_name} --dir apps/{app_name}
```
**Python:**
```bash
# Crear main.py con sys.path al registry
# Import pattern: sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))
```
**Bash:**
```bash
# Crear main.sh con source a funciones del registry
# Pattern: source "$REGISTRY_ROOT/bash/functions/{domain}/{func}.sh"
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
```
### Inicializar operations.db
```bash
cd /home/lucas/fn_registry
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
```
### Indexar en registry.db
```bash
cd /home/lucas/fn_registry && ./fn index
# Verificar
sqlite3 registry.db "SELECT id, name, lang, domain FROM apps WHERE name = '{app_name}';"
```
---
## PASO 3: EJECUTAR — Verificar que funciona (@fn-executor)
Invocar al agente **fn-executor** para:
1. Verificar que la app compila/ejecuta correctamente
2. Configurar entities y relations en operations.db si la app maneja datos
3. Ejecutar una primera ejecucion de prueba
4. Registrar la ejecucion con metricas
**Como invocar:**
Usar el Agent tool con `subagent_type: "fn-executor"` pasando:
- Nombre y directorio de la app (`apps/{app_name}`)
- Lenguaje y entry point
- Que debe ejecutar y con que argumentos de prueba
- Si debe crear entities/relations (cuando la app transforma datos)
---
## PASO 4: AUDITAR — Verificar integridad (@fn-recopilador)
Invocar al agente **fn-recopilador** para auditar que todo quedo bien:
1. Estructura de la app (app.md, operations.db, .gitignore)
2. Schema de operations.db completo
3. Integridad de datos (entities, relations, executions)
4. Coherencia con registry.db (uses_functions, type_refs)
5. App indexada correctamente
**Como invocar:**
Usar el Agent tool con `subagent_type: "fn-recopilador"` pasando:
- Nombre de la app a auditar
- Que es una app nueva y debe verificar todo desde cero
Si el recopilador detecta problemas, corregirlos antes de continuar.
---
## PASO 5: PUBLICAR en Gitea (@gitea) — OBLIGATORIO
Toda app nueva DEBE publicarse en Gitea. Este paso NO es opcional.
**Como invocar:**
Usar el Agent tool con `subagent_type: "gitea"` pasando:
- Crear repo `{app_name}` en la organizacion `dataforge` de Gitea
- La URL base de Gitea: `https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com`
- Inicializar el repo con el contenido de `apps/{app_name}/`
- El repo debe tener su propio `.git` independiente del fn_registry
**Pasos que el agente gitea debe ejecutar:**
```bash
# 1. Crear repo en Gitea (via API)
# 2. Inicializar git en la app
cd /home/lucas/fn_registry/apps/{app_name}
git init
git add -A
git commit -m "Initial commit: {app_name} — {descripcion}"
# 3. Configurar remote y push
git remote add origin https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/{app_name}.git
git push -u origin master
# 4. Actualizar repo_url en app.md
```
**Despues de publicar**, actualizar el `repo_url` en app.md y re-indexar:
```bash
cd /home/lucas/fn_registry && ./fn index
```
---
## PASO 6: Resumen final
Reportar al usuario:
```
=== APP CREADA: {app_name} ===
Directorio: apps/{app_name}/
Lenguaje: {lang}
Dominio: {domain}
Framework: {framework}
Entry point: {entry_point}
Funciones del registry usadas:
- {id1}: {descripcion}
- {id2}: {descripcion}
Funciones nuevas creadas:
- {id3}: {descripcion}
Operations:
Entities: N
Relations: N
Executions: N (primera ejecucion: {status})
Repo Gitea: {repo_url}
Para ejecutar:
cd apps/{app_name} && {comando_ejecucion}
```
---
## Flujos segun tipo de app
### App Go TUI (Bubbletea)
1. Consultar funciones TUI existentes: `sqlite3 registry.db "SELECT id, description FROM functions WHERE domain = 'tui' ORDER BY name;"`
2. Crear app con framework bubbletea
3. Estructura: main.go + app/model.go + views/ + config/
4. Tag `launcher` en app.md si debe aparecer en Pipeline Launcher
### App Go Desktop (Wails)
1. Usar `scaffold_wails_app_go_infra` para generar el proyecto
2. Consultar componentes Wails del registry: `sqlite3 registry.db "SELECT id, description FROM functions WHERE id LIKE '%wails%' ORDER BY name;"`
3. Frontend usa @fn_library (Mantine v9, @tabler/icons-react)
4. Bindings Go via `wails_bind_crud_go_infra`
### App Python
1. Consultar funciones Python: `sqlite3 registry.db "SELECT id, description FROM functions WHERE lang = 'py' AND domain = 'DOMINIO' ORDER BY name;"`
2. Import pattern con sys.path al registry
3. Deps con requirements.txt o pyproject.toml
### App Bash
1. Consultar funciones Bash: `sqlite3 registry.db "SELECT id, description FROM functions WHERE lang = 'bash' ORDER BY name;"`
2. Source pattern con REGISTRY_ROOT
3. set -euo pipefail obligatorio
### App C++ (ImGui)
1. Codigo fuente va en `apps/{app_name}/` (no en `cpp/apps/`)
2. `cpp/CMakeLists.txt` referencia la app con `add_subdirectory(../apps/{app_name} ...)`
3. Funciones C++ del registry se incluyen como .cpp en el CMakeLists.txt de la app
4. Para Windows: cross-compile con `cmake -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake`
### Service (tag `service`)
1. Detectar si el usuario pide un servicio (API, daemon, watcher, server) o usa prefijo `service:`
2. Añadir tag `service` al array `tags` del app.md
3. Documentar en app.md: puerto, como lanzar/parar, health check
4. Estructura tipica para un HTTP service en Go:
```
apps/{service_name}/
├── app.md # tags: [service, api, ...]
├── main.go # Bind port, listen, graceful shutdown
├── handlers.go # HTTP handlers que componen funciones del registry
├── go.mod
├── .gitignore
```
5. El service se ejecuta como: `go run . --port 8080`
6. Para consultar services existentes: `sqlite3 registry.db "SELECT id, name, description FROM apps WHERE tags LIKE '%service%';"`
---
## Reglas
- **Codigo reutilizable** va en `functions/`, NO en la app → usar fn-constructor
- **Codigo especifico** de la app va en `apps/{app_name}/`
- **Todas las apps van en `apps/`**, incluidas C++, TypeScript, etc. Nunca en `cpp/apps/` ni otros subdirectorios
- **operations.db** SOLO dentro de la app, NUNCA en la raiz
- **registry.db** SOLO en la raiz, NUNCA en apps
- Toda app DEBE tener `app.md` con frontmatter completo
- `uses_functions` en app.md DEBE listar TODAS las funciones del registry importadas
- Siempre `./fn index` despues de crear/modificar la app — **verificar que aparece en registry.db**
- Siempre auditar con fn-recopilador antes de publicar
- **Siempre publicar en Gitea** (PASO 5) — toda app tiene repo en `dataforge/{app_name}`
- **Siempre actualizar `repo_url`** en app.md despues de publicar y re-indexar
- **Tag `service`**: añadir a apps que son procesos de larga duracion (APIs, daemons, watchers, schedulers)
- **Tag `launcher`**: añadir a pipelines que deben aparecer en Pipeline Launcher TUI
$ARGUMENTS
+121
View File
@@ -0,0 +1,121 @@
# /autonomous-task — Lanza fn-orquestador (Fase 6 del ciclo reactivo)
Lanza el meta-orquestador autonomo que recorre el bucle CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR sobre un issue, sin intervencion humana, hasta convergencia / estancamiento / timeout / limite de iteraciones.
Issue 0069. Pre-condiciones obligatorias (chequear ANTES de despachar):
1. Migration `fn_operations/migrations/006_task_runs.sql` aplicada.
2. Subagentes `fn-constructor`, `fn-executor`, `fn-recopilador`, `fn-analizador`, `fn-mejorador`, `fn-orquestador` presentes en `.claude/agents/`.
3. `dev/autonomous_protected_paths.json` existe.
4. `master` local up-to-date con `origin/master`.
5. Branch `auto/<issue_id>` NO existe ya.
6. `gh auth status` OK (necesario para PR draft al converger).
7. Tipo de tarea soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`.
Si alguna pre-condicion falla → ABORT con razon. NO improvisar.
---
## Argumento
`$ARGUMENTS``<issue_id>` o `<task_spec_path>` + flags opcionales.
```
/autonomous-task 0070
/autonomous-task 0070 --max-iterations 15 --max-minutes 90
/autonomous-task 0070 --auto-apply-proposals safe
/autonomous-task 0070 --dry-run
/autonomous-task path/to/spec.yaml --branch auto/custom-name
```
Flags:
- `--max-iterations N` tope de iteraciones (default 10)
- `--max-minutes M` timeout total (default 60)
- `--auto-apply-proposals` `none|safe|aggressive` (default `safe`)
- `--branch NAME` rama TBD (default `auto/<issue_id>`)
- `--dry-run` simula, NO aplica
---
## Comportamiento
1. **Verificar pre-condiciones** con script bash (ver arriba). Si alguna falla, reportar y salir.
2. **Despachar a `fn-orquestador`** via Agent tool con `subagent_type=fn-orquestador`. Pasar:
- `issue_id` o `task_spec`
- flags resueltos
- paths protegidos (leidos de `dev/autonomous_protected_paths.json`)
3. **El subagente:**
- Crea worktree aislado `/tmp/fn_orq_<issue>_<ts>/` desde `master`.
- Persiste estado en `task_runs` (operations.db del app target o repo root).
- Despacha por fases a los 5 subagentes especializados.
- Aplica proposals filtradas por `--auto-apply-proposals`.
- Termina con: `converged` (PR draft creado) | `stalled` | `timeout` | `iterations_exhausted` | `needs_human` | `aborted`.
4. **Reportar resultado al humano** con:
- `status`, `iterations / max`, `duration / max`
- `branch`, `worktree`, `PR draft url` si converged
- `proposals creadas / aplicadas`
- `last run_id` y status
- Resumen iter-por-iter del `progress_json`
---
## Reglas duras (no negociables)
- Sandbox de rama EN WORKTREE — nunca toca master ni el working tree del humano.
- No merge automatico — PR draft siempre.
- No `--no-verify`, no `--force`, no skip hooks.
- Paths protegidos via `dev/autonomous_protected_paths.json`.
- Watchdog: 2 iteraciones con mismo set de fails → `status=stalled`.
- Auditoria total en `task_runs.progress_json`.
- No self-modification: NO toca `.claude/agents/` ni `.claude/commands/`.
---
## Integracion con call_monitor (issue 0085)
El orquestador puede leer `projects/fn_monitoring/apps/call_monitor/operations.db` para:
- Consultar `function_stats` antes de decidir que funciones usar/reusar.
- Filtrar proposals existentes via `mcp__registry__fn_proposal --status pending` para evitar duplicados.
- Loggear sus invocaciones via el hook PostToolUse (automatico).
Tras converger, el `call_monitor propose` ejecutado por el humano (o futuro cron) absorbera las nuevas violations / copied_code / fails para alimentar la siguiente ronda.
---
## Tipos NO soportados
- Diseño arquitectura nuevo (humano decide).
- Decisiones UX subjetivas.
- Cambios BD productiva.
- Cualquier cosa que toque secrets/credenciales.
- Self-modification del propio orquestador.
Si el issue contiene criterios no-verificables programaticamente, ABORT con `status=needs_human`.
---
## Output canonico
```
=== /autonomous-task: 0070 ===
status: converged
iterations: 7 / 10
duration: 23 min / 60
branch: auto/0070
worktree: /tmp/fn_orq_0070_1731612345
PR draft: https://github.com/.../pull/123
proposals: 3 creadas, 2 auto-aplicadas
last run_id: e2e_run_abc123 (status: pass)
Iter:
1. construir → ok (2 funciones nuevas)
2. ejecutar → ok
3. analizar → fail (2/8 checks)
4. mejorar → 3 proposals (2 auto-applicadas)
5. construir → ok (re-build tras patches)
6. analizar → pass
7. recopilador → ok (operations.db integra)
Siguiente: revisar PR draft + fn proposal list -s pending --target-id 0070
```
+37
View File
@@ -0,0 +1,37 @@
# /compile — Compila app C++ y la copia al escritorio de Windows
Wrapper sobre el pipeline `compile_cpp_app_bash_pipelines`. Toda la lógica vive en el registry (resolver app desde CWD/arg, cross-compile MinGW, copiar exe + DLLs + assets/ + enrichers/ + runtime/ a `/mnt/c/Users/lucas/Desktop/apps/<app>/`, taskkill previo, preservar `local_files/`).
```bash
cd /home/lucas/fn_registry
./fn run compile_cpp_app "$ARGUMENTS"
```
## Argumento
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`).
- Sin argumento: deduce desde `pwd` si estás dentro de `cpp/apps/<X>/` o `projects/*/apps/<X>/`.
- Si no se puede deducir y no se pasa argumento, el pipeline lista las apps disponibles en stderr y aborta.
## Qué hace el pipeline
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>` desde arg o CWD.
2. Verifica `CMakeLists.txt` en el dir resuelto.
3. `build_cpp_windows_bash_infra <app>` — cross-compila el target específico con `cpp/build/windows/` (configura toolchain `mingw-w64.cmake` la primera vez).
4. `deploy_cpp_exe_to_windows_bash_infra <app> <dir>`:
- `taskkill.exe /IM <app>.exe /F` (pre-autorizado).
- Copia `<app>.exe` + DLLs al top-level de `Desktop/apps/<app>/`.
- rsync `cpp/build/windows/apps/<app>/assets/``Desktop/apps/<app>/assets/`.
- rsync `<app_dir>/enrichers/``assets/enrichers/` si existe.
- Si `app.md` declara `python_runtime: true`, regenera `runtime/` con `tools/freeze_python_runtime.sh` y rsync a `assets/runtime/`.
- Copia `gx-cli`/`gx-cli.exe` si existen.
- **NUNCA** toca `local_files/` (estado del usuario).
5. Imprime `ls -lh` del `.exe` final.
## Notas
- Solo target Windows hoy. Android / Linux quedan fuera (Linux ya lo da `cpp/build/`).
- Variables override-ables: `BUILD_WIN`, `WIN_DESKTOP_APPS`, `FN_REGISTRY_ROOT`.
- Si la app no está registrada en `cpp/CMakeLists.txt`, `cmake --build --target <app>` falla. Registrar siguiendo `.claude/rules/cpp_apps.md` §5.
- Para tocar la lógica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,deploy_cpp_exe_to_windows,compile_cpp_app}.sh`, no este wrapper.
+273
View File
@@ -0,0 +1,273 @@
# /create_functions — Crear funciones para el registry a partir de una peticion
Eres un agente orquestador que evalua una peticion del usuario, consulta el registry, planifica las funciones necesarias y las crea en paralelo usando agentes fn-constructor especializados. Tambien creas unit tests y verificas que todo quedo indexado correctamente.
---
## Argumento
`$ARGUMENTS` — descripcion de lo que el usuario necesita. Ejemplos:
```
/create_functions funciones para parsear y validar JSON schema en Go
/create_functions pipeline Python para ETL de CSVs con filtrado y agregacion
/create_functions funciones de hashing y encoding para ciberseguridad en Go
/create_functions componentes React para formularios con validacion
/create_functions funciones Bash para gestion de contenedores Docker
```
Si `$ARGUMENTS` esta vacio, preguntar al usuario que funciones necesita.
---
## FASE 1: EVALUAR — Entender la peticion
1. **Parsear la peticion** para identificar:
- Dominio(s) involucrados (core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser, notebook, ui)
- Lenguaje(s) preferido(s) (go, py, bash, typescript). Si no se especifica, inferir del contexto.
- Tipo de funciones necesarias: puras (algoritmos, transformaciones), impuras (I/O, red, DB), pipelines (composiciones), tipos, componentes
- Nivel de granularidad: funciones atomicas vs composiciones
2. **Si la peticion es ambigua**, preguntar al usuario SOLO lo esencial (no mas de 2 preguntas).
---
## FASE 2: OBSERVAR — Consultar el registry
Consultar `registry.db` para encontrar funciones existentes relevantes y evitar duplicados.
```bash
# Buscar funciones similares por nombre y descripcion (OBLIGATORIO — usar multiples terminos)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
# Buscar tipos relacionados
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
# Funciones del dominio objetivo
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
# Tipos del dominio objetivo
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
# Funciones que podrian componerse (misma firma de retorno)
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
```
**Clasificar resultados en:**
- **Reutilizables directamente**: funciones que ya hacen lo que se necesita
- **Componibles**: funciones que pueden usarse como building blocks
- **Similares pero diferentes**: funciones parecidas que confirman que no hay duplicado exacto
---
## FASE 3: PLANIFICAR — Disenar las funciones con un agente Plan
Invocar el Agent tool con `subagent_type: "Plan"` para disenar la lista de funciones a crear.
El prompt al agente Plan debe incluir:
- La peticion original del usuario
- Las funciones existentes encontradas en FASE 2 (IDs y descripciones)
- Los tipos existentes relevantes
- Las reglas de pureza del registry
El agente Plan debe producir una lista estructurada de funciones a crear, cada una con:
- **nombre** (snake_case)
- **kind** (function | pipeline | component)
- **lang** (go | py | bash | typescript)
- **domain**
- **purity** (pure | impure) — justificando por que
- **signature** propuesta
- **description** breve
- **uses_functions** — IDs de funciones existentes que reutiliza
- **uses_types** — IDs de tipos existentes que usa
- **dependencias** — si una funcion nueva depende de otra funcion nueva del mismo batch, indicar el orden
- **tests** — que se debe testear (casos de exito, edge cases, errores)
**Reglas del plan:**
- Funciones puras primero, impuras despues, pipelines al final
- Maximizar reutilizacion de funciones existentes
- Cada funcion debe tener tests propuestos
- El plan debe indicar el **orden de creacion** (las que tienen dependencias internas van despues)
- Agrupar funciones independientes para creacion en paralelo
**NO pedir confirmacion al usuario** — proceder directamente a la fase de construccion. Mostrar el plan brevemente en el output como referencia pero sin pausar:
---
## FASE 4: CONSTRUIR — Crear funciones en paralelo con fn-constructor
Para cada batch del plan, lanzar agentes `fn-constructor` **en paralelo** (un agente por funcion o grupo pequeno de funciones relacionadas).
**Como invocar cada fn-constructor:**
Usar el Agent tool con `subagent_type: "fn-constructor"` pasando un prompt completo con:
```
Crea la siguiente funcion para el registry fn_registry en /home/lucas/fn_registry:
Funcion: {nombre}
Kind: {kind}
Lang: {lang}
Domain: {domain}
Purity: {purity}
Signature: {signature}
Description: {descripcion}
Uses_functions: [{ids}]
Uses_types: [{ids}]
Tests requeridos:
- {test1}: {descripcion del test}
- {test2}: {descripcion del test}
- {test3}: {descripcion del test}
Contexto: Esta funcion es parte de un batch para {descripcion general del objetivo}.
Funciones existentes del registry que puedes reutilizar: {ids relevantes}
IMPORTANTE:
- Crear el archivo de codigo Y el .md con frontmatter completo
- Crear el archivo de tests correspondiente
- Marcar tested: true en el .md si creas tests
- Respetar las reglas de pureza
- Usar tipos nativos en la firma
- file_path relativo a la raiz del registry
- NO ejecutar fn index (lo hare yo al final)
```
**Orden de ejecucion:**
1. Lanzar todos los fn-constructor del Batch 1 en paralelo
2. Esperar a que terminen
3. Lanzar todos los fn-constructor del Batch 2 en paralelo (dependen de Batch 1)
4. Repetir para cada batch subsiguiente
**Sin limite de agentes en paralelo** — lanzar todos los fn-constructor del batch simultaneamente para maxima velocidad.
---
## FASE 5: INDEXAR — Registrar todo en el registry
Despues de que TODOS los fn-constructor terminen:
```bash
# Indexar todo de una vez
cd /home/lucas/fn_registry && ./fn index
```
Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
- ID duplicado → renombrar
- uses_functions referencia ID inexistente → verificar que el batch anterior se creo correctamente
- Violacion de pureza → ajustar purity o quitar dependencia impura
- file_path incorrecto → corregir la ruta
---
## FASE 6: VERIFICAR — Asegurar que todo esta correcto
### 6.1 Verificar indexacion
```bash
# Verificar cada funcion creada
cd /home/lucas/fn_registry
./fn show {id_de_cada_funcion}
# Verificar que no hay funciones sin params_schema
./fn check params
```
### 6.2 Ejecutar tests
Para cada funcion con tests, ejecutar:
```bash
cd /home/lucas/fn_registry
# Go
CGO_ENABLED=1 go test -tags fts5 -v -run TestNombreDelTest ./functions/{domain}/
# Python
python/.venv/bin/python3 -m pytest python/functions/{domain}/{nombre}_test.py -v
# TypeScript
cd frontend && pnpm exec vitest run functions/{domain}/{nombre}.test.ts
# Bash (si hay tests)
bash bash/functions/{domain}/{nombre}_test.sh
```
### 6.3 Verificar integridad
```bash
# Verificar que todas las funciones nuevas estan en la BD
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
# Verificar que los tests estan indexados
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
# Verificar dependencias
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
```
### 6.4 Si algo fallo
- Si un test falla → corregir el codigo y re-ejecutar
- Si una funcion no se indexo → verificar el .md y re-indexar
- Si hay errores de integridad → corregir y re-indexar
- NO continuar al reporte si hay tests fallando o funciones sin indexar
---
## FASE 7: REPORTE — Resumen final
```
=== FUNCIONES CREADAS ===
Peticion: {descripcion original}
Funciones del registry reutilizadas:
- {id}: {descripcion}
Funciones nuevas:
- {id} [{kind}, {purity}, {lang}] — {descripcion}
Tests: N pasando
Archivo: {file_path}
- {id} [{kind}, {purity}, {lang}] — {descripcion}
Tests: N pasando
Archivo: {file_path}
Tipos nuevos:
- {id}: {descripcion}
Tests: X/Y pasando
Indexacion: OK
Para usar estas funciones:
# Go
import "fn_registry/functions/{domain}"
result := domain.FunctionName(args)
# Python
from {domain} import function_name
# Bash
source "$FN_REGISTRY_ROOT/bash/functions/{domain}/{name}.sh"
```
---
## Reglas
- **SIEMPRE** consultar registry.db antes de crear — evitar duplicados
- **NO pedir confirmacion** — mostrar el plan brevemente y proceder directamente
- **SIEMPRE** crear tests para cada funcion
- **SIEMPRE** indexar y verificar despues de crear
- **Funciones puras primero**, impuras despues, pipelines al final
- **Maximizar paralelismo** en la creacion (agentes fn-constructor en paralelo)
- **Maximizar reutilizacion** de funciones existentes
- **NO crear funciones especificas de una app** — solo codigo reutilizable y generico
- Si el usuario pide algo que ya existe, informar y sugerir reutilizar en vez de duplicar
- Si una funcion del batch falla, las demas del mismo batch pueden continuar independientemente
- **Tags con significado especial** — ver `.claude/rules/function_tags.md`:
- `launcher`: pipelines que deben aparecer en Pipeline Launcher TUI. Añadir cuando se crea un pipeline ejecutable desde el launcher. NO añadir a pipelines interactivos/TUIs.
- `service`: para apps que son procesos de larga duracion (usado en /app, no en funciones)
$ARGUMENTS
+260
View File
@@ -0,0 +1,260 @@
# /documentar — Distribuir la conversacion en la documentacion del registry
Documenta la **conversacion actual** repartiendo el contenido en TODOS los `.md` que correspondan: artefactos del registry (funciones, tipos, apps, projects, analysis, vaults) **y documentacion global del repo** (`docs/*`, `docs/adr/`, `CHANGELOG.md`, `dev/issues/*`, `.claude/rules/*`, `.claude/CLAUDE.md`, sub-CLAUDEs, READMEs/SPECs en apps). Cierra con una entrada en `/entrada_diario`. El objetivo es que **otro LLM (o yo en otra sesion) pueda continuar** sin haber visto la conversacion: contexto, decisiones, gotchas, paths, IDs, comandos exactos, "lo siguiente que pega".
## Uso
```
/documentar # documenta todo lo relevante de la sesion
/documentar shaders_lab fase 6 # acota a artefactos/temas concretos (opcional)
```
`$ARGUMENTS` es opcional: si va vacio, documenta toda la sesion. Si lleva texto, usalo como hilo conductor para decidir que es relevante.
---
## Reglas duras
1. **NUNCA** escribir secretos en ningun `.md` ni en el diario:
- Passwords, tokens, API keys, GPG keys, ssh private keys, valores reales de variables de entorno sensibles (`REGISTRY_API_TOKEN`, `*_SECRET`, `*_PASSWORD`, `*_TOKEN`, basicAuth en URLs).
- Si el usuario lo pide explicitamente, OK. Por defecto, redactar como `<token>` / `<password>` o referenciar el origen (`pass entry registry_api`, `~/.fn_pc`).
- URLs publicas, hosts, puertos, paths, IDs, nombres de servicios, env var **names** (no values), licencias, hashes de commit cortos: SI se documentan.
2. **NUNCA** sobreescribir secciones existentes ni reordenar contenido previo. Solo **append** o seccion nueva con timestamp/fase si encaja.
3. **SIEMPRE** consultar `registry.db` con FTS5 para encontrar el `.md` correcto antes de editar (no asumir paths).
4. **SIEMPRE** cerrar invocando `/entrada_diario` con un resumen del bloque (a no ser que el usuario diga lo contrario).
5. **Densidad util > prosa**: comandos exactos, IDs del registry, paths relativos, error messages literales, flags de build, decisiones (con el "porque"), bugs encontrados (con el fix), proximos pasos. Sin fluff.
---
## PASO 0 — Recopilar el material de la sesion
Antes de escribir nada, repasar la conversacion y juntar:
1. **Artefactos tocados** (creados, editados, ejecutados, mencionados):
- Funciones / tipos del registry → IDs `{name}_{lang}_{domain}`.
- Apps (`apps/*` o `projects/*/apps/*`).
- Projects (`projects/*`).
- Analyses (`analysis/*` o `projects/*/analysis/*`).
- Vaults (`projects/*/vaults/vault.yaml`).
- Reglas (`.claude/rules/*.md`), ADRs (`docs/adr/*.md`), templates (`docs/templates/*`).
- Issues (`dev/issues/*.md`, `dev/issues/completed/*.md`).
- Docs globales (`docs/*.md`: architecture, integrity, execution_standard, fn_operations, sync_setup, init-pipelines, testing, functions, types, fn-registry-system-complete).
- CLAUDE.md raiz (`.claude/CLAUDE.md`) y sub-CLAUDEs (`apps/*/.claude/CLAUDE.md`, `projects/*/apps/*/.claude/CLAUDE.md`, `analysis/*/.claude/CLAUDE.md`, `projects/*/analysis/*/.claude/CLAUDE.md`).
- Docs sueltas en apps (`apps/*/SPEC.md`, `apps/*/README.md`, `apps/*/docs/*.md`, `cpp/DESIGN_SYSTEM.md`).
- `CHANGELOG.md` raiz.
2. **Cambios concretos** desde git:
```bash
cd /home/lucas/fn_registry
git status --short
git diff --stat
git log --since="6 hours ago" --oneline
```
Cada path modificado mapea a un artefacto — convertir a su `.md`.
3. **Material no codigo** que vale la pena dejar registrado:
- Decisiones de diseño y por que (anti-bitrot: el porque suele perderse).
- Bugs encontrados + raiz + fix (no solo "fix").
- Atajos / convenciones nuevas.
- Pendientes y "lo siguiente que pega" para la proxima sesion.
- Aprendizajes operativos (build flags, cross-compile gotchas, env requerido).
4. **Filtrar secretos** segun la regla dura #1.
Si el material es solo conversacion exploratoria sin artefactos tocados, ir directo a PASO 4 (solo diary).
---
## PASO 1 — Mapear cada bloque de informacion a su `.md`
Para cada artefacto identificado, localizar su `.md` consultando `registry.db`:
```bash
cd /home/lucas/fn_registry
# Funcion / tipo
sqlite3 registry.db "SELECT id, file_path FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:NAME* OR description:NAME*');"
sqlite3 registry.db "SELECT id, file_path FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:NAME* OR description:NAME*');"
# App / project / analysis (los .md son nombres fijos)
sqlite3 registry.db "SELECT id, dir_path FROM apps WHERE name = 'NAME';" # → {dir_path}/app.md
sqlite3 registry.db "SELECT id, dir_path FROM projects WHERE name = 'NAME';" # → projects/NAME/project.md
sqlite3 registry.db "SELECT id, dir_path FROM analysis WHERE name = 'NAME';" # → {dir_path}/analysis.md
sqlite3 registry.db "SELECT id, name, path FROM vaults WHERE name = 'NAME';" # → vault.yaml entry
```
Si el `.md` aun no existe (artefacto recien creado en la sesion y todavia no indexado), el path se deduce de la convencion:
- Funcion: `functions/{domain}/{name}.md`, `python/functions/{domain}/{name}.md`, `bash/functions/{domain}/{name}.md`, `frontend/functions/{domain}/{name}.md`, `cpp/functions/{domain}/{name}.md`.
- Tipo: `types/{domain}/{name}.md` (codigo en `functions/{domain}/{name}.go`).
- App: `apps/{name}/app.md` o `projects/{proyecto}/apps/{name}/app.md`.
- Project: `projects/{name}/project.md`.
- Analysis: `analysis/{name}/analysis.md` o `projects/{proyecto}/analysis/{name}/analysis.md`.
### Donde escribir dentro de cada `.md`
| Tipo de `.md` | Seccion preferida para append |
|-----------------------|----------------------------------------------------------------------------------------------------------------|
| Funcion / tipo | `## Notas` al final. Si no existe, crearla. NO tocar el frontmatter salvo que el usuario pida cambiar metadata. |
| App (`app.md`) | `## Estado actual` con sub-fases si el app ya las usa (ej. `### Fase 7 — ... [done]`). Si no, `## Notas`. Tambien `## Lo siguiente que pega` para futuros pasos. |
| Project (`project.md`)| `## Notas` o seccion del area afectada (`## Apps`, `## Operacion`, `## Troubleshooting`). |
| Analysis (`analysis.md`)| `## Notas` o `## Hallazgos` (crearla si no existe). |
| Vault (`vault.yaml`) | Comentario al final del entry o crear `vaults/{name}/README.md` con notas operativas (NO meter datos sensibles). |
| Regla (`.claude/rules/*`)| Solo si el usuario explicitamente formaliza una regla nueva — entonces archivo nuevo + entrada en `INDEX.md`. |
| ADR (`docs/adr/*`) | Solo si la decision es arquitectural y persistente — archivo nuevo numerado. |
### Documentacion global / cross-cutting (NO saltarse)
Estos `.md` describen el sistema entero, no un artefacto concreto. Cuando un cambio impacta convenciones, comportamiento de agentes, decisiones, issues abiertos o features visibles al usuario, **tambien** se actualizan aqui:
| Archivo / carpeta | Cuando tocarlo | Como |
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| `CHANGELOG.md` (raiz) | Cambio visible al usuario o agentes: nueva funcion/pipeline/app, breaking change, fix relevante, rename, deprecate. | Append bajo seccion del dia (`## YYYY-MM-DD`) con `### Added/Changed/Fixed/Removed/Deprecated`. NUNCA reescribir entradas previas. Si es trabajo en curso, usar `## [Unreleased]`. |
| `docs/adr/NNNN-slug.md` | **Decision arquitectural** persistente con alternativas descartadas (no es regla operativa, es historia del por que). | Archivo nuevo numerado siguiendo plantilla en `docs/adr/README.md`. Estado inicial: `accepted` o `proposed`. |
| `docs/architecture.md` | Cambia la arquitectura general (BDs, layers, flujo de datos, capas). | Append en seccion afectada o nueva subseccion. Mantener tablas y diagramas existentes. |
| `docs/integrity.md` | Nueva regla de integridad / referencia cruzada que el indexer valida. | Append a la lista de reglas. Reflejar tambien en codigo del indexer si toca. |
| `docs/execution_standard.md` | Cambia el estandar de ejecucion (`fn run`, despacho por lenguaje, env vars). | Append seccion. Sincronizar con `.claude/CLAUDE.md` si menciona los mismos comandos. |
| `docs/sync_setup.md` | Cambia el flujo de `fn sync`, env vars (`FN_REGISTRY_API`, `REGISTRY_API_TOKEN`), `~/.fn_pc`, troubleshooting. | Append. Recordatorio: NO escribir el valor del token, solo el nombre. |
| `docs/init-pipelines.md` | Nuevo pipeline de scaffolding o cambio en uno existente. | Append seccion del pipeline. |
| `docs/testing.md` | Cambia convencion de tests, runners, layout de `*_test.go`/`test_*.py`. | Append seccion afectada. |
| `docs/functions.md` / `docs/types.md` | Cambia el schema de la tabla `functions` o `types` (columnas, FTS5, enums, `params_schema`). | Append. Sincronizar con `.claude/CLAUDE.md` schema rapido. |
| `docs/fn_operations.md` | Cambia el schema/comportamiento de `operations.db` o el bucle reactivo (entities, relations, executions, assertions). | Append seccion afectada. |
| `docs/fn-registry-system-complete.md` | Snapshot completo del sistema — solo si la sesion implico un rediseño grande. Normalmente NO se toca por sesion. | Si toca, append seccion con timestamp. |
| `docs/templates/*.md` | Cambia el frontmatter obligatorio de un tipo de artefacto (function/pipeline/component/type/app/project/analysis). | Editar la plantilla correspondiente. Tambien actualizar ejemplos en `.claude/CLAUDE.md`. |
| `dev/issues/NNNN-*.md` | Sesion trabajo en un issue: progreso, blockers, decisiones del scope. | Append `## Notas / Progreso` con timestamp. NO mover de `dev/issues/` a `dev/issues/completed/` salvo que el issue cierre. |
| `dev/issues/completed/NNNN-*.md` | Issue completado en esta sesion. | Mover el archivo a `completed/` (`git mv`) y actualizar la fila en `dev/issues/README.md` (estado `completado`, link a `completed/...`). |
| `dev/issues/README.md` | Issue creado, cambia estado, prioridad, dependencias. | Editar la fila correspondiente o anadir nueva al final de la tabla. |
| `.claude/rules/*.md` + `INDEX.md` | El usuario formaliza una nueva regla operativa. | Archivo nuevo + fila en `INDEX.md`. Numerar en el indice manteniendo orden. |
| `.claude/CLAUDE.md` (raiz) | Cambio en convenciones globales del proyecto, comandos `fn` nuevos, env vars, estructura de carpetas, schema BDs. | Append en seccion afectada. Sincronizar con `docs/` si hay overlap. |
| Sub-CLAUDE (`apps/*/.claude/CLAUDE.md`, `analysis/*/.claude/CLAUDE.md`, `projects/*/apps/*/.claude/CLAUDE.md`) | Cambio especifico en como un agente debe trabajar dentro de esa app/analysis (no global). | Append. NO duplicar reglas que ya estan en CLAUDE.md raiz. |
| `cpp/DESIGN_SYSTEM.md` | Cambia tokens, layout, primitivas visuales del stack C++. | Append seccion afectada. |
| `apps/*/SPEC.md`, `apps/*/README.md`, `apps/*/docs/*.md`, `apps/*/NEXT_STEPS_*.md` | App tiene docs propias mas alla de `app.md`. | Append. Si el contenido encaja mejor en `app.md`, preferir `app.md` y mencionar el SPEC desde ahi. |
### Reglas de decision rapidas
- **¿Cambio visible / breaking / nueva feature?** → `CHANGELOG.md` SI.
- **¿Decision con alternativas descartadas?** → ADR SI. Una regla operativa "haz X" sin alternativas → `.claude/rules/`.
- **¿Cambia como un agente debe comportarse?** → `.claude/rules/` o `.claude/CLAUDE.md` (global) o sub-CLAUDE (local).
- **¿Cambia el schema de BDs o columnas?** → `docs/functions.md`/`docs/types.md`/`docs/fn_operations.md` + `.claude/CLAUDE.md` schema rapido.
- **¿Trabajo en un issue?** → `dev/issues/NNNN-*.md` + tabla en `dev/issues/README.md`.
- **¿Cross-cutting sin artefacto y sin encajar arriba?** → solo diario (PASO 4).
---
## PASO 2 — Escribir las actualizaciones
Para cada `.md` identificado:
1. `Read` el archivo para ver estructura actual y secciones.
2. Decidir si **append a seccion existente** o **crear seccion nueva**.
3. Usar `Edit` para append (preferible) o `Write` solo si es archivo nuevo.
4. **Mantener el estilo** del archivo (markdown, viñetas cortas, bloques de codigo con lenguaje).
5. **No tocar el frontmatter** salvo que el usuario haya cambiado metadata explicita (`description`, `tags`, `uses_functions`, `version`). Si se toca, re-indexar al final.
### Plantilla de bloque para append en `.md` de artefacto
```markdown
### {Fase / Tema corto} `[done|wip|notes]`
{1-3 lineas de contexto: que se hizo y por que.}
- Hecho: {cambio concreto, con path y/o ID si aplica}.
- Hecho: {cambio concreto}.
- Bug + fix: {sintoma → raiz → fix} (si procede).
- Decision: {opcion elegida vs alternativa} — porque {razon} (si procede).
- Pendiente: {algo que queda} (si procede).
{Comando(s) exacto(s) si la operacion vale la pena reproducir.}
```
### Plantilla para `## Lo siguiente que pega` (apps maduras)
```markdown
- {Tarea proxima}: {contexto minimo, que tocar, criterio de hecho}.
```
---
## PASO 3 — Reindexar si tocaste frontmatter o creaste artefacto
Si los cambios de la sesion incluyen creacion de funciones/tipos/apps/projects/analysis/vaults o modificacion de frontmatter:
```bash
cd /home/lucas/fn_registry && ./fn index
```
Y verificar:
```bash
./fn show {id_creado_o_modificado}
```
Si solo se editaron secciones de prosa (Notas, Estado actual, etc.) sin tocar frontmatter, el indexado igual recoge `documentation`/`notes` actualizados — re-indexar es barato y deja la BD coherente.
---
## PASO 4 — Cerrar con entrada al diario
Invocar `/entrada_diario` con un resumen conciso de la sesion (3-6 viñetas, verbos en pasado para lo hecho, infinitivo para pendientes). Referenciar:
- IDs de artefactos tocados.
- Paths relativos clave.
- Hashes de commit cortos si la sesion termino con commits.
- ADRs / issues / proposals abiertos.
Ejemplo de invocacion:
```
/entrada_diario shaders_lab fase 6 — menubar reusable (View + Layouts) cableado, persistencia de layouts en shaders_lab.db
```
Si el usuario ya invoco `/entrada_diario` antes en esta sesion para este bloque, **no duplicar**: solo añadir lo que no estaba.
---
## PASO 5 — Reportar al usuario
Resumen breve (formato texto, no tabla a no ser que sean muchos):
```
=== DOCUMENTADO ===
Artefactos (.md de registry):
- apps/shaders_lab/app.md (Fase 6 — menubar)
- cpp/functions/core/app_menubar.md (notas de uso)
- cpp/functions/core/layouts_menu.md (notas de cableado)
Globales:
- CHANGELOG.md (Added: app_menubar, layouts_menu)
- docs/adr/0002-menubar-arch.md (nuevo ADR — decision menubar reusable)
- dev/issues/README.md (issue 0027 → completado)
- dev/issues/completed/0027-...md (movido)
- .claude/rules/cpp_icons.md (regla nueva, anadida a INDEX.md)
Diario: docs/diary/2026-04-25.md (## 18:30 — ...)
Re-indexado: si | no
Pendientes registrados: {N}
Secretos omitidos: {lista de tipos redactados, ej. "REGISTRY_API_TOKEN"}
```
---
## Checklist final
- [ ] Cada artefacto tocado tiene su `.md` actualizado (append, no overwrite).
- [ ] Ningun secreto, password, token o key en los archivos.
- [ ] Comandos exactos, IDs y paths para que otro LLM reproduzca.
- [ ] Decisiones con su "porque", bugs con su raiz+fix.
- [ ] `CHANGELOG.md` actualizado si el cambio es visible al usuario / agentes.
- [ ] ADR creado (`docs/adr/NNNN-*.md`) si hay decision arquitectural con alternativas descartadas.
- [ ] `dev/issues/*.md` y `dev/issues/README.md` actualizados si la sesion toco issues.
- [ ] `docs/{architecture,integrity,functions,types,fn_operations,execution_standard,sync_setup,init-pipelines,testing}.md` actualizado si el cambio afecta lo que cada uno documenta.
- [ ] `.claude/CLAUDE.md` raiz actualizado si cambian convenciones / comandos / schema globales.
- [ ] `.claude/rules/*` + `INDEX.md` actualizado si el usuario formaliza una regla nueva.
- [ ] Sub-CLAUDE de la app/analysis afectada actualizado si cambia comportamiento agente local.
- [ ] `/entrada_diario` invocado con resumen de la sesion.
- [ ] `./fn index` corrido si hubo creacion o cambio de frontmatter.
- [ ] Reporte final al usuario con la lista de archivos tocados (artefactos + globales).
$ARGUMENTS
+211
View File
@@ -0,0 +1,211 @@
# /e2e-cpp — Crear/ejecutar tests e2e para apps C++
Genera y corre tests e2e con **Dear ImGui Test Engine** sobre las apps C++ del registry. Cada app gana un ejecutable `<app>_tests` que reabre la app dentro de un harness de testing y ejecuta scripts de UI (clicks, escritura, asserts) sobre los componentes ImGui.
Suite ya instalada en `cpp/vendor/imgui_test_engine/`. Integracion en framework: `fn::run_app_test()` (ver `cpp/framework/app_base.h`). Opt-in via `-DFN_BUILD_TESTS=ON`. Sin la opcion los builds normales de `/compile` no cambian.
## Argumento
`$ARGUMENTS` — formato libre. Casos:
- `<app_name>` — solo el nombre. Si la app ya tiene tests, los ejecuta. Si no, pide al usuario que describa el flujo a testear.
- `<app_name> <descripcion del flujo>` — genera un test nuevo para ese flujo y lo ejecuta. Ej: `chart_demo abrir cada tab y verificar que renderiza`.
- vacio — detectar app desde `pwd` (si estas en `cpp/apps/<X>/` o `projects/*/apps/<X>/`); si no, listar apps disponibles.
## Pasos
### 1. Resolver app y directorio
```bash
ROOT=/home/lucas/fn_registry
ARGS="$ARGUMENTS"
APP_ARG="${ARGS%% *}" # primera palabra
FLOW_DESC="${ARGS#* }" # resto (puede coincidir con APP_ARG si solo hay una palabra)
[ "$FLOW_DESC" = "$APP_ARG" ] && FLOW_DESC=""
# Detectar desde CWD si no hay arg
if [ -z "$APP_ARG" ]; then
CWD="$(pwd)"
case "$CWD" in
"$ROOT"/cpp/apps/*|"$ROOT"/projects/*/apps/*)
APP_ARG="$(basename "$CWD")" ;;
esac
fi
if [ -z "$APP_ARG" ]; then
echo "Apps C++ disponibles:"
ls "$ROOT"/cpp/apps/ 2>/dev/null
ls "$ROOT"/projects/*/apps/ 2>/dev/null
echo "Uso: /e2e-cpp <app> [descripcion del flujo]"
exit 1
fi
APP_DIR=""
for cand in "$ROOT/cpp/apps/$APP_ARG" "$ROOT"/projects/*/apps/"$APP_ARG"; do
[ -d "$cand" ] && [ -f "$cand/CMakeLists.txt" ] && APP_DIR="$cand" && break
done
[ -z "$APP_DIR" ] && { echo "App C++ no encontrada: $APP_ARG"; exit 1; }
echo "App: $APP_ARG"
echo "Dir: $APP_DIR"
```
### 2. Inspeccionar la app
Lee:
- `$APP_DIR/main.cpp` — identifica:
- El nombre de la funcion principal de render (suele ser `render()` o `static void render()`).
- El **window title** que aparece en `ImGui::Begin("...")` — sera el primer arg de `ctx->SetRef("...")` en los tests. Si tiene em-dash u otros UTF-8 no ASCII, anotar la secuencia de bytes (ej: `\xe2\x80\x94` para `—`).
- Los IDs/labels de los widgets candidatos: tabs (`BeginTabItem`), botones (`Button`), inputs (`InputText`), checkboxes, etc.
- `$APP_DIR/app.md` — para entender el dominio y proposito.
- `$APP_DIR/CMakeLists.txt` — para saber que `.cpp` del registry enlaza la app (los tests linkearan los mismos).
### 3. Decidir tests a escribir
**Si `$FLOW_DESC` esta vacio**: pregunta al usuario que flujo testear. Sugiere 2-3 candidatos basados en los widgets vistos en main.cpp. NO inventes flujos sin confirmacion.
**Si `$FLOW_DESC` viene en el comando**: convierte la descripcion en una secuencia de pasos atomicos del Test Context API. Ejemplos canonicos:
| Descripcion humano | Llamada Test Engine |
|---|---|
| "abrir tab X" | `ctx->ItemClick("##tabs/X")` o el path real del TabBar |
| "escribir 'hola' en el input search" | `ctx->ItemInput("Search", "hola")` |
| "click boton Aceptar" | `ctx->ItemClick("Aceptar")` |
| "verificar que aparece el modal Y" | `IM_CHECK(ctx->WindowInfo("Y").ID != 0)` |
| "checkbox Z marcado" | `IM_CHECK(ctx->ItemIsChecked("Z"))` |
| "menu File > Open" | `ctx->MenuClick("File/Open")` |
Ver `cpp/vendor/imgui_test_engine/imgui_te_context.h` para el catalogo completo de helpers.
### 4. Preparar la app para tests (idempotente)
Si es la primera vez que la app gana tests, hay que:
**a) Hacer la funcion render() linkable desde otra TU**
```cpp
// Antes: static void render() { ... }
// Despues: void render() { ... }
```
**b) Excluir `int main()` con guarda `FN_TEST_BUILD`**
```cpp
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({...}, render);
}
#endif
```
Verifica con `grep -n "FN_TEST_BUILD\|^static void render" "$APP_DIR/main.cpp"`. Si ya esta, no toques nada.
### 5. Generar/extender el archivo de tests
`$APP_DIR/tests/<app>_tests.cpp` — un solo archivo por app, varias `IM_REGISTER_TEST` dentro de `register_tests()`.
**Plantilla**:
```cpp
// E2E tests para <app> — Dear ImGui Test Engine.
// Construido solo con -DFN_BUILD_TESTS=ON. Reusa el mismo main.cpp con
// FN_TEST_BUILD definido para excluir su int main().
#include "app_base.h"
#include "imgui.h"
#include "imgui_te_engine.h"
#include "imgui_te_context.h"
void render(); // definido en <app>/main.cpp
static void register_tests(ImGuiTestEngine* e) {
ImGuiTest* t = nullptr;
t = IM_REGISTER_TEST(e, "<app>", "<test_name>");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("<window_title_exacto>");
// ... pasos del flujo
};
// mas tests aqui
}
int main() {
fn::AppConfig cfg{};
cfg.title = "<app>_tests";
cfg.width = 1280;
cfg.height = 800;
return fn::run_app_test(cfg, render, register_tests);
}
```
Si el archivo ya existe: **AGREGA** un nuevo `IM_REGISTER_TEST` dentro de la funcion `register_tests` existente. NO sobreescribas tests previos.
### 6. Actualizar CMakeLists.txt (idempotente)
Si `$APP_DIR/CMakeLists.txt` no tiene aun el bloque de tests, agregar al final:
```cmake
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
if(FN_BUILD_TESTS)
add_imgui_app(<app>_tests
main.cpp
tests/<app>_tests.cpp
# mismos .cpp del registry que la app principal
${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp
...
)
target_compile_definitions(<app>_tests PRIVATE FN_TEST_BUILD)
endif()
```
Las fuentes deben replicar las del target principal (mismas funciones del registry). Si la app ya tiene un bloque `if(FN_BUILD_TESTS)`, no lo dupliques.
### 7. Build
```bash
cd "$ROOT/cpp"
cmake -S . -B build/linux_tests -DFN_BUILD_TESTS=ON 2>&1 | tail -5
cmake --build build/linux_tests --target ${APP_ARG}_tests -j4 2>&1 | tail -20
```
Si el build falla:
- Errores de compilacion en `tests/...cpp` → revisa nombres de widgets/paths con el codigo real de main.cpp.
- "undefined reference to render" → falta quitar `static` o falta el `#ifndef FN_TEST_BUILD` en main.cpp.
- "multiple definition of main" → falta el `target_compile_definitions(... FN_TEST_BUILD)` en CMakeLists.
### 8. Ejecutar (headless en WSL)
WSL no tiene GLX 4.3 nativo — los tests corren bajo `xvfb` con software renderer Mesa. Wrapper canonico:
```bash
cd "$ROOT/cpp/build/linux_tests"
TEST_BIN="$(find . -name "${APP_ARG}_tests" -type f -executable | head -1)"
[ -z "$TEST_BIN" ] && { echo "no encuentro el binario de tests"; exit 1; }
timeout 90 xvfb-run -a -s "-screen 0 1280x800x24" \
env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
"$TEST_BIN" 2>&1
EXIT=$?
echo "EXIT: $EXIT"
```
Si en el host el usuario tiene GL nativo y `DISPLAY` funciona, el wrapper xvfb-run sigue siendo seguro (ejecuta dentro de su propio display).
### 9. Reportar
- Si `EXIT == 0` y la salida contiene `Tests Result: OK` → reporta `N/M tests passed` con la lista de tests ejecutados.
- Si `EXIT != 0` → muestra el bloque de log del test fallido (test engine imprime el path del widget que no encontro, el archivo y la linea del IM_CHECK que fallo). Sugiere correcciones (widget renombrado, path mal escrito, race entre frames — usar `ctx->Yield()`).
### 10. Despues de añadir tests
NO ejecutes `fn index` automaticamente — los tests no son funciones del registry, son artefactos de la app. Si el usuario los queria persistir, ya los tiene en `<app_dir>/tests/`.
Si la app es un sub-repo (lo normal segun ADR 0002), recordar al usuario que los archivos nuevos viven dentro del repo de la app y necesitan un commit alli (no en `fn_registry`).
## Referencias
- API de Test Context: `cpp/vendor/imgui_test_engine/imgui_te_context.h`
- API del engine: `cpp/vendor/imgui_test_engine/imgui_te_engine.h`
- Implementacion del harness: `cpp/framework/app_base.cpp` (funcion `fn::run_app_test`)
- Ejemplo canonico: `cpp/apps/chart_demo/tests/chart_demo_tests.cpp`
- Licencia del test engine: personal/open-source gratis (`cpp/vendor/imgui_test_engine/LICENSE.txt`)
+47
View File
@@ -0,0 +1,47 @@
# /entrada_diario — Añadir entrada al diario del día
Wrapper sobre `append_diary_entry_bash_infra`. La función del registry maneja todo el manejo de archivos (crear `docs/diary/YYYY-MM-DD.md` si no existe, append seguro, formato exacto). Este comando solo decide el contenido.
## Uso
```
/entrada_diario <descripción del bloque de trabajo>
/entrada_diario # sin args → resume sesión actual
```
## Pasos del asistente
1. **Componer `TITULO` (corto, una linea) y `CUERPO`** (viñetas markdown):
- Con `$ARGUMENTS`: derivar `TITULO` directo del argumento; `CUERPO` con viñetas concretas (`- Hecho:`, `- Pendiente:`).
- Sin `$ARGUMENTS`: revisar TaskList + `git log --since=today` + `git status` y resumir en 3-5 viñetas.
2. **Llamar la función del registry**:
```bash
cd /home/lucas/fn_registry
source bash/functions/infra/append_diary_entry.sh
append_diary_entry "<TITULO>" "$(cat <<'EOF'
<CUERPO>
EOF
)"
```
La función imprime el path del archivo escrito.
## Reglas de estilo
- Viñetas breves, no párrafos. Verbos en pasado para lo hecho, infinitivo para pendientes.
- Enlaces a artefactos: commits (SHA corto 7-8 chars), ADRs (`[0001](../adr/0001-...)`), funciones del registry por ID.
- No duplicar con CHANGELOG: el diario es contexto operativo ("qué hice hoy"), el CHANGELOG es "qué cambió cara al usuario".
- NUNCA editar secciones anteriores. La función solo append.
## Relación con otras formas de registro
| Si quieres documentar... | Usa |
|--------------------------|-----|
| Qué trabajé hoy | `/entrada_diario` → `docs/diary/` |
| Qué cambió en el código (cara usuario/agentes) | Editar `CHANGELOG.md` directamente |
| Por qué tomamos una decisión arquitectural | Nuevo ADR en `docs/adr/NNNN-*.md` |
| Una regla operativa nueva del registry | Nuevo archivo en `.claude/rules/` + entrada en INDEX.md |
## Para tocar la lógica
Editar la función `append_diary_entry_bash_infra` en el registry, no este wrapper.
+281
View File
@@ -0,0 +1,281 @@
# /extract-design — Mejorar @fn_library con exports de Claude Design
Eres un agente mejorador del design system. Tu trabajo es analizar un export "standalone" de Claude Design (`sources/frontend_designs/*.html`), identificar componentes nuevos o mejoras sobre `@fn_library`, aplicarlos al registry y propagarlos al espejo público `subrepos/fn-design-system` (GitHub + Gitea).
**Objetivo:** cada diseño exportado debería dejar el registry un poco mejor que antes. Lo que Claude Design inventó para cubrir un hueco hoy → componente reutilizable del registry mañana.
---
## Argumento
`$ARGUMENTS` — ruta al `.html` en `sources/frontend_designs/`. Si no se proporciona:
1. Lista los `.html` bajo `sources/frontend_designs/` ordenados por fecha.
2. Muestra fecha + nombre + tamaño.
3. Pregunta cuál procesar. Default: el más reciente.
---
## PASO 0 — Validar input
```bash
ls -lht sources/frontend_designs/*.html 2>/dev/null
```
Si no existe el fichero, abortar. Si existe, leer las primeras líneas para confirmar que es un export de Claude Design (`__bundler/manifest`, `__bundler/template` en el HTML).
---
## PASO 1 — Decodificar el bundle
Ejecutar el extractor:
```bash
python3 .claude/scripts/extract_design_bundle.py \
"sources/frontend_designs/<NOMBRE>.html" \
"sources/frontend_designs/<NOMBRE>_extracted/"
```
Esperado: directorio con `app.jsx`, `fn_library_emu.jsx`, `charts_emu.jsx`, `data.jsx` + fuentes woff2 + `manifest.json`.
Si falta alguno de los 4 `.jsx` clave, inspeccionar por UUID; puede que Claude Design haya usado estructura distinta. Reportar al usuario.
---
## PASO 2 — Inventariar el diseño
Leer `app.jsx` y listar **todos los componentes React definidos** (funciones que empiezan con mayúscula o usan `function Xxx(`). Categorizar:
### 2a. Componentes del export que YA existen en `@fn_library`
- Grep el barrel: `cat frontend/functions/ui/index.ts | grep "^export"`.
- Para cada componente del export, ver si aparece en el barrel. Registrar coincidencias.
### 2b. Componentes nuevos (no existen en el registry)
Componentes React del `app.jsx` cuyo nombre no aparece en el barrel. Estos son **candidatos a extracción**.
### 2c. Uso de variantes / props no documentadas
Leer `fn_library_emu.jsx` del export y comparar API con tus `.tsx` reales:
```bash
# Comprobar componentes específicos si el export los usa con props nuevas
sqlite3 registry.db "SELECT id, signature, props FROM functions WHERE id = 'alert_ts_ui';"
```
Anotar discrepancias (variantes faltantes, props nuevas, tipos distintos).
### 2d. Datos/patrones reutilizables en `data.jsx`
- RNG determinista (mulberry32) → candidato a `frontend/functions/core/rng_seeded_ts_core` o `python/functions/core/`.
- Helpers tipo `statusBadge()` → documentar como receta, no como componente.
---
## PASO 3 — Consultar el registry para evitar duplicados
Para cada componente candidato del paso 2b, búsqueda FTS5 antes de proponerlo:
```bash
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:<CANDIDATO>* OR description:<PALABRAS_CLAVE>') ORDER BY name;"
```
Si encuentras algo similar que pueda ser mejorado en lugar de duplicado, márcalo como **mejora** a ese existente.
---
## PASO 4 — Presentar el diagnóstico al usuario
Muestra en tablas separadas:
### 🟢 Componentes nuevos candidatos
| # | Nombre propuesto | Dominio | Líneas | Reutilizable en | API |
|---|---|---|---|---|---|
| 1 | `funnel_chart_ts_ui` | ui | ~35 | CRM, analytics, funnels genéricos | `(data: Array<{stage, value}>, variant?) → JSX` |
### 🟡 Mejoras a componentes existentes
| # | Componente | Mejora | Tipo | Riesgo |
|---|---|---|---|---|
| A | `alert_ts_ui` | Añadir variantes `success`, `warning`, `info` | Expandir enum | Bajo — no rompe API |
| B | `data_table_ts_ui` | Prop `density: 'compact'|'cozy'|'roomy'` | Añadir prop opcional | Bajo |
### 🔵 Patrones a documentar (no componente)
| Patrón | Dónde registrar |
|---|---|
| `statusBadge` helper | `DESIGN_SYSTEM.md` sección "patterns" |
**Esperar confirmación.** El usuario responde con sintaxis `1,2,A,B` (o `all`, o `nuevos only`, o descarta algunos). Si dice `all`, aplica todo lo listado.
---
## PASO 5 — Aplicar mejoras aprobadas
### 5a. Para componentes nuevos (candidatos 🟢)
Por cada aprobado:
1. **Leer código** del `app.jsx` / `fn_library_emu.jsx` / `charts_emu.jsx` del export.
2. **Adaptar al stack real del registry:**
- Cambiar elementos SVG/HTML planos por primitivas de `@mantine/core` cuando corresponda (`Paper`, `Stack`, `Group`, `Text`).
- Cambiar `style={{...}}` por props Mantine (`p`, `m`, `fw`, `gap`, `radius`, `c`).
- Si es un chart, delegar en `@mantine/charts` cuando sea posible; solo usar SVG puro si Mantine no cubre el caso (ej: `Sparkline` en el registry ya es SVG puro por rendimiento).
- Iconos: `@tabler/icons-react`.
3. **Crear los dos ficheros** siguiendo la convención:
- `frontend/functions/ui/<name>.tsx` — código React.
- `frontend/functions/ui/<name>.md` — frontmatter completo.
4. **Frontmatter del .md** (campos clave):
```yaml
id: <name>_ts_ui
name: <name>
kind: component
lang: ts
domain: ui
purity: impure
framework: react
version: 1.0.0
description: "..."
tags: [...]
props: {...}
emits: null
params: []
output: "JSX.Element — ..."
source_repo: "claude.ai/design"
source_license: ""
source_file: "sources/frontend_designs/<NOMBRE>.html"
file_path: frontend/functions/ui/<name>.tsx
tested: false
```
5. **Añadir al barrel** `frontend/functions/ui/index.ts`: `export { Xxx } from './<name>'`.
### 5b. Para mejoras a componentes existentes (🟡)
Por cada aprobada:
1. **Leer** el `.tsx` actual.
2. **Aplicar la mejora** sin romper la API existente: añade prop opcional, amplía enum de `variant`, etc.
3. **Actualizar** el `.md` correspondiente para reflejar las nuevas variantes/props (campos `variant`, `props`, `description`).
4. **Si la firma cambia**, actualizar también el `signature` del frontmatter.
### 5c. Para patrones a documentar (🔵)
1. Añadir una sección "Patterns" en `frontend/DESIGN_SYSTEM.md` si no existe.
2. Registrar el patrón con un ejemplo corto.
---
## PASO 6 — Indexar y verificar
```bash
./fn index
```
- Si falla por integridad, arreglar y reintentar.
- Verificar cada componente nuevo: `./fn show <id>`.
- Confirmar que el barrel compila haciendo `cd frontend && pnpm tsc --noEmit` (si tarda, al menos verificar imports manualmente).
---
## PASO 7 — Sincronizar al espejo
```bash
cd subrepos/fn-design-system
./sync_from_registry.sh
git add -A
git status --short # Mostrar qué cambió en el espejo
```
Si hay cambios, preparar commit. Si no, el sync no recogió las modificaciones — investigar.
---
## PASO 8 — Commit en ambos repos
### 8a. Commit en `fn_registry`
```bash
git add frontend/functions/ui/ frontend/DESIGN_SYSTEM.md registry.db
git commit -m "$(cat <<'EOF'
feat(ui): extract <N> components / <M> improvements from design export
From: sources/frontend_designs/<NOMBRE>.html
New components:
- <id> — <descripción corta>
- ...
Improvements:
- <id> — <cambio>
- ...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
### 8b. Commit en el espejo
```bash
cd subrepos/fn-design-system
git commit -m "sync: <N> new components + <M> improvements from <NOMBRE>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```
---
## PASO 9 — Push
### 9a. Push del espejo (ambos remotes)
```bash
cd subrepos/fn-design-system
./push_all.sh
```
Esto propaga a:
- `gitea/dataforge/fn-design-system`
- `github/gutierenmanuel/fn-design-system` ← este es el que Claude Design consume
### 9b. Push de fn_registry
**Preguntar al usuario** antes — no push sin permiso (ver CLAUDE.md del proyecto). Si dice sí:
```bash
git push origin master
```
---
## PASO 10 — Resumen final
Mostrar al usuario:
```
✓ Extracción completa.
Nuevos componentes en @fn_library:
- <id> (frontend/functions/ui/<name>.tsx)
- ...
Mejoras aplicadas:
- <id>: <qué cambió>
Espejo actualizado:
- Commit gitea: <sha> → <url>
- Commit github: <sha> → <url>
Claude Design verá estas mejoras en su próxima lectura del repo enlazado.
Siguiente acción sugerida: probar un prompt de dashboard que use <componente_nuevo>.
```
---
## Reglas críticas
- **NUNCA extraer sin aprobación explícita del usuario** — siempre paso 4 con tabla y espera.
- **NUNCA sobrescribir un componente existente** en el paso 5b — solo añadir variantes/props opcionales. Si la mejora es incompatible, proponerlo como propuesta aparte (`fn proposal add`) en vez de aplicarla.
- **SIEMPRE `source_repo: "claude.ai/design"`** en el frontmatter de componentes nuevos, y `source_file` apuntando al `.html` original.
- **SIEMPRE mantener el orden:** registry → index → verify → sync mirror → commit both → push mirror → (ask to push fn_registry).
- **El barrel `index.ts`** debe estar actualizado antes de hacer `fn index` (hay apps que lo importan).
- **NO committear** `operations.db*`, `node_modules/`, `dist/`, `.env` ni nada que `.gitignore` excluya. Usa `git add` con rutas explícitas, no `git add -A` a ciegas.
- **Si el usuario cancela a mitad**, dejar el working tree limpio o documentar qué quedó pendiente. No medio-commits.
- **Patrones que no tienen sentido como primitiva** (ej. envs, branding específico) → documentar, no componentizar.
+215
View File
@@ -0,0 +1,215 @@
# /extract-source — Extraer funciones de un repo en sources/
Eres un agente extractor de funciones. Tu trabajo es analizar un repositorio clonado en `sources/` y extraer funciones reutilizables al registry siguiendo las reglas de `.claude/rules/sources.md`.
---
## Argumento
`$ARGUMENTS` — nombre del directorio en `sources/` (ej: `MiroFish`, `OpenViking`). Si no se proporciona, listar los directorios disponibles en `sources/` y pedir al usuario que elija.
---
## PASO 0: Validar el source
```bash
ls sources/$ARGUMENTS/
```
Si no existe, abortar. Verificar que tenga licencia compatible (MIT, Apache 2.0, BSD, ISC, MPL-2.0, Unlicense). Si es AGPL, GPL, o no tiene licencia, **advertir al usuario** y pedir confirmacion antes de continuar.
Identificar:
- **Licencia**: leer LICENSE/LICENSE.md/COPYING
- **Lenguaje principal**: detectar por archivos (*.go, *.py, *.rs, *.ts, *.js, Cargo.toml, go.mod, pyproject.toml, package.json)
- **URL del repo**: buscar en README, .git/config, o package.json
---
## PASO 1: Revisar el manifest
Leer `sources/sources.yaml` para ver si este repo ya tiene extracciones previas. Si las tiene, listarlas al usuario y preguntar si quiere continuar extrayendo mas o si quiere re-evaluar las existentes.
---
## PASO 2: Explorar el repositorio
Analizar la estructura del repo para identificar **todas las funciones candidatas** — puras e impuras. El objetivo es maximizar la extraccion de codigo util.
### Que buscar (por categoria)
**A. Funciones puras** (algoritmos, transformaciones, calculos, validaciones):
- Parsers, encoders/decoders, formatters
- Algoritmos matematicos, estadisticos, financieros
- Transformaciones de datos, filtros, mappers
- Validaciones, sanitizaciones
**B. Funciones impuras** (I/O, red, estado externo):
- Clientes HTTP/API (REST, GraphQL, WebSocket)
- Operaciones de filesystem (leer, escribir, monitorear archivos)
- Interacciones con bases de datos (queries, migraciones)
- Operaciones Docker, cloud, infraestructura
- Scraping, crawling, recoleccion de datos
- Notificaciones, envio de mensajes
**C. Pipelines** (composiciones multi-paso):
- Flujos ETL (extract-transform-load)
- Workflows de setup/deploy/provision
- Secuencias de procesamiento de datos
- Orquestaciones que componen varias funciones
**D. Tipos reutilizables** (structs, enums, interfaces):
- Modelos de dominio genericos
- Tipos de configuracion
- Interfaces/protocolos bien definidos
### Estrategia de exploracion segun lenguaje
- **Go**: `pkg/`, `internal/`, `utils/`, `lib/`, `cmd/` — funciones exportadas, handlers, clients
- **Python**: `src/`, `lib/`, `utils/`, `core/`, `api/` — funciones, clases client, decoradores
- **Rust**: `crates/`, `src/lib.rs` — funciones pub, traits implementados
- **TypeScript/JS**: `src/`, `lib/`, `utils/`, `services/` — funciones, hooks, componentes
- **Bash**: `scripts/`, `bin/`, `tools/` — funciones con firma clara
### Que ignorar
- main(), CLI entry points (pero extraer las funciones que invocan)
- Tests (pero notar cuales funciones estan bien testeadas — marcar `tested: true`)
- Funciones que dependen de tipos internos complejos **no adaptables**
- Codigo con dependencias externas pesadas que no esten en fn_registry
- Config loaders hardcodeados a un proyecto especifico
---
## PASO 3: Consultar el registry para evitar duplicados
Antes de proponer cualquier funcion, buscar en registry.db con FTS5:
```bash
# Por cada candidata, buscar similares
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:NOMBRE* OR description:DESCRIPCION') ORDER BY name;"
```
Si ya existe algo similar, descartarla o anotar que es una mejora/variante.
---
## PASO 4: Presentar candidatas al usuario
Agrupar las candidatas por categoria y mostrar en tablas separadas:
### Funciones puras
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | Descripcion |
|---|---|---|---|---|---|
### Funciones impuras
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | I/O tipo | Descripcion |
|---|---|---|---|---|---|---|
(I/O tipo: HTTP, filesystem, DB, Docker, network, etc.)
### Pipelines (composiciones)
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | Funciones que compone | Descripcion |
|---|---|---|---|---|---|---|
### Tipos
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | Algebraic | Descripcion |
|---|---|---|---|---|---|---|
Para cada candidata indicar:
- Por que cumple el filtro de calidad
- Si requiere adaptacion (renombrar tipos, quitar dependencias, traducir lenguaje)
- Si es traduccion de otro lenguaje (ej: Rust → Go)
- Para impuras: cual es el `error_type` apropiado
**Esperar confirmacion del usuario** antes de extraer. El usuario puede:
- Aprobar todas (`all`)
- Seleccionar por numero (`1,3,5-8`)
- Seleccionar por categoria (`todas las puras`, `solo pipelines`)
- Pedir explorar mas areas del repo
- Descartar y terminar
---
## PASO 5: Extraer funciones aprobadas
Para cada funcion aprobada:
### 5a. Determinar destino y clasificacion
| Naturaleza | Destino | kind | purity |
|---|---|---|---|
| Algoritmo/logica pura | Go/Python `functions/{domain}/` | function | pure |
| Funcion con I/O (HTTP, DB, fs) | Go/Python `functions/{domain}/` | function | impure |
| Script/utilidad sistema | Bash `bash/functions/{domain}/` | function | impure |
| UI/componente | TypeScript `frontend/functions/{domain}/` | component | — |
| Composicion multi-paso | `functions/pipelines/` o `python/functions/pipelines/` | pipeline | impure |
| C/Rust/otro lenguaje | Traducir a Go o Python manteniendo semantica | segun caso | segun caso |
### 5b. Crear archivos
1. **Codigo** — copiar y adaptar:
- Renombrar a snake_case
- Usar tipos nativos en firma (no tipos internos del repo)
- Quitar dependencias externas, usar stdlib
- Ajustar al paquete Go destino (nombre = nombre del directorio)
- Si es traduccion, mantener la semantica y documentar el origen
2. **Metadata .md** — crear frontmatter completo:
- `source_repo`: URL del repo original
- `source_license`: licencia del repo
- `source_file`: path relativo del archivo original dentro del repo
- Todos los campos obligatorios segun el tipo (function/pipeline/component)
- Reglas de pureza:
- `pure``returns_optional: false` + `error_type: ""`
- `impure``error_type: "error_go_core"` (o equivalente Python)
- `pipeline``purity: impure` + `uses_functions` con las funciones que compone
### 5c. Verificar integridad
```bash
# Indexar
./fn index
# Verificar cada funcion extraida
./fn show {id}
```
Si el indexer reporta errores, corregir antes de continuar.
---
## PASO 6: Actualizar manifest
Anadir las funciones extraidas a `sources/sources.yaml` bajo el repo correspondiente:
```yaml
- repo: https://github.com/user/project
license: MIT
cloned_dir: nombre_directorio
extracted:
- id: funcion_go_core
source_file: pkg/utils.go
date: YYYY-MM-DD # fecha de hoy
```
Si el repo no existe en el manifest, crear la entrada completa.
---
## PASO 7: Resumen
Mostrar al usuario:
- Funciones extraidas exitosamente (con IDs)
- Funciones descartadas y por que
- Warnings del indexer si hubo
- Sugerencia de areas del repo que podrian explorarse en el futuro
---
## Reglas criticas
- **NUNCA extraer sin aprobacion del usuario** — siempre presentar candidatas primero
- **NUNCA ignorar el filtro de calidad** — si no cumple todos los criterios, no se extrae
- **SIEMPRE consultar registry.db** antes de proponer — evitar duplicados
- **SIEMPRE atribuir** — source_repo, source_license, source_file en el .md
- **SIEMPRE actualizar sources.yaml** — es el manifest versionado
- **Licencias no permisivas** (GPL, AGPL) requieren advertencia explicita al usuario
- **Traduccion de lenguaje** es valida — documentar el origen claramente
+226
View File
@@ -0,0 +1,226 @@
---
description: "Auto-auditoria: verifica que la sesion registra uso de funciones, detecta gaps (patrones inline repetidos, wrappers saltados, heredocs sin function_id), lanza fn-constructor en paralelo para crear las funciones que faltan, y valida que Claude usara las nuevas en el siguiente turno"
---
# /fn_claude — auto-auditoria + auto-construccion del registry
Comando meta: Claude se audita a si mismo. Verifica que su comportamiento en esta sesion (y las recientes) deja rastro en `call_monitor.operations.db`, detecta gaps reales del registry para el trabajo actual, lanza sub-agentes `fn-constructor` en paralelo para cerrar esos gaps, y verifica que la proxima vez usara las funciones nuevas.
## Objetivos del registry (Norte) — Issues 0086 + 0087
Cada corrida de `/fn_claude` optimiza 4 metricas visibles en Monitor tab del `registry_dashboard`:
1. **MAXIMIZAR `Reg %`** — % de calls con `function_id != ''`. Cada heredoc/bash que reescribe logica baja el ratio. Target: subir cada semana.
2. **MEJORAR uso del registry por Claude** — Claude busca y reusa antes de escribir. `MCP` (mcp/heredoc/fn run) sube, `violations` baja. Si una funcion existe pero Claude no la encuentra, mejorar su `description`/`tags`/`params_schema` (FTS indexa todo).
3. **ACELERAR tareas comunes** — patrones inline repetidos >2x -> `fn-constructor` los convierte en funcion, Claude las usa el siguiente turno. Menos pasos por tarea = mas valor.
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry crece **promoviendo secuencias A->B(->C) que se repiten con exito** a pipelines one-shot. Una funcion que hace bien una cosa NO necesita crecer. Pattern detection: `call_monitor sequences --detect --propose` (cron 6h activo) + tab `Promotion candidates` del dashboard.
Si `/fn_claude` no mueve estas 4 metricas, no esta haciendo su trabajo.
## Infraestructura de discovery activa (issue 0087)
Cada turno tienes capacidades ya cargadas SIN buscar. Si no las usas estas pagando el coste de FTS innecesariamente:
| Senal | Donde | Que hacer |
|---|---|---|
| Linea `CAPABILITIES (cache 1h): TOP: ... FRESH (7d): ... PIPELINES: ...` en cada UserPromptSubmit | hook `hook_capabilities_inject.sh` | Antes de buscar con `mcp__registry__fn_search`, mira si la funcion que necesitas esta en TOP/FRESH/PIPELINES. Si si, ve directo a `fn show <id>` (1 read) o `./fn run <id>` (0 reads). |
| `<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function. USE: ./fn run <id> -> <signature>` aparecido mid-flight | hook `hook_fn_match.sh` (PreToolUse, Bash matcher) | El hook detecto que tu Bash inline coincide con una funcion del registry. **NO ignores el reminder** — abandona el inline, llama a `./fn run <id>` o `mcp__registry__fn_run id="<id>"`. Si crees que la sugerencia es falso positivo, justifica brevemente antes de seguir inline (queda en violations). |
| Hint AUSENTE para una query corta (`rsi sma` < 3 tokens) | threshold `raw_score >= 4.0` no alcanzado | NO interpretar la ausencia de hint como "no existe funcion". Usa `mcp__registry__fn_search` con query mas rica (3+ tokens del dominio). |
| Falso positivo conocido: `agent` token | `robots.txt user-agent` matchea `agent_scaffold` | Ignora el reminder y sigue. Cost = 1 reminder ignorable. |
## Como combinar la 3 senales para minimizar pasos
1. **User prompt llega** -> lees `CAPABILITIES` line. Si la tarea encaja claramente con TOP/FRESH -> usa directo.
2. **Vas a escribir Bash inline** -> el hook PreToolUse lo intercepta. Si dispara FUZZY-MATCH -> usa `./fn run <id>`.
3. **No hay match y necesitas codigo** -> `mcp__registry__fn_search` con 3+ tokens. Si sigue sin hit -> delega a `fn-constructor` (no escribas inline). Patron repetido detectado por `call_monitor sequences` se promovera a pipeline en proximas iteraciones.
## Las 4 metricas norte (donde vigilarlas)
- `Reg %` (Monitor KPI) — % calls con function_id no vacio. Sube cuando el registry se usa.
- `MCP` (Monitor KPI) — count calls con tools registry-aware (mcp*/heredoc*/fn_cli_run). Adopcion de patrones canonicos.
- `Errors` / `Violations` (Monitor KPI) — bajan cuando el bucle cierra.
- `Failed Functions` (Monitor sub-tab) — registry-functions que fallaron: diagnostico de bugs prioritarios.
Issue 0085 fase autocompleta. Reemplaza el flujo manual de "veo un patron, decido si extraer, escribo proposal, espero humano, fn-mejorador genera, fn-orquestador opera". Con `/fn_claude` Claude hace todo eso solo, **autonomamente para si mismo**.
---
## Comportamiento (ejecutalo en este orden)
### 1. AUDIT — ¿estoy siendo registrado?
```bash
ROOT="/home/lucas/fn_registry"
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
# Pre-condiciones
[ -f "$MON" ] || { echo "call_monitor.operations.db NO existe — issue 0085a no aplicado"; exit 1; }
[ "$FN_TELEMETRY" = "1" ] || echo "WARNING: FN_TELEMETRY != 1 — wrappers Python/Bash inactivos"
# Metricas de la sesion actual + ultimas 24h
sqlite3 "$MON" <<SQL
SELECT 'calls_session', COUNT(*) FROM calls WHERE session_id = '${CLAUDE_SESSION_ID:-unknown}'
UNION ALL SELECT 'calls_24h', COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)
UNION ALL SELECT 'violations_24h', COUNT(*) FROM violations WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)
UNION ALL SELECT 'tool_used_distribution_24h', NULL;
SELECT tool_used, COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER) GROUP BY tool_used ORDER BY 2 DESC;
SQL
```
Si `calls_session = 0` → algo esta mal (hook PostToolUse no fire o BD no escribible). Reporta y para.
Si `mcp_*` / total < 0.4 → estas usando demasiado heredoc/sqlite directo. Reporta como warning.
### 2. GAP — ¿que funciones faltan?
Dos fuentes:
#### 2a. Patrones repetidos en heredocs/Edit
```sql
-- En call_monitor.operations.db
SELECT tool_used, COUNT(*) AS hits
FROM calls
WHERE function_id = ''
AND ts >= CAST(strftime('%s','now','-7 days') AS INTEGER)
AND tool_used IN ('heredoc_py', 'heredoc_bash', 'sqlite_direct')
GROUP BY tool_used;
```
Si `heredoc_py > 5` sin function_id → Claude esta componiendo logica que probablemente debe ser pipeline. Investigar el ultimo heredoc del transcript: si reescribe algo que ya es funcion del registry → violation candidate. Si no, es candidato a pipeline nuevo.
#### 2b. Trabajo actual de la sesion — gap inferido del contexto
Lee el ultimo prompt del usuario y los ultimos 10 turnos. Lista funciones que:
- Has llamado inline (sed/awk/jq custom, transformaciones de datos, parsing).
- Has reinventado (HTTP client raw, SQLite open con flags, FS walks).
- Has compuesto >2 veces con el mismo shape.
Para cada candidato:
```bash
# Verifica si ya existe algo similar en el registry
mcp__registry__fn_search "<keyword del candidato>"
```
Si NO existe match relevante → candidato a `fn-constructor`.
Si existe pero firma incompleta → candidato a `improve_function` (proposal, NO auto-construccion).
### 3. PROPOSE — lista candidatos
Genera tabla:
```
| Candidato | Razon | Lenguaje | Dominio | Evidencia (snippet) |
|---|---|---|---|---|
| <name> | inline_repeated/wrapper_skip/new | go/py/bash | core/infra/... | <heredoc fragment> |
```
Si lista vacia → "no gaps detected, sesion saludable" + reporta metricas. Para.
### 4. CONSTRUCT — lanza fn-constructor en paralelo
Para cada candidato, dispara un sub-agente `fn-constructor` con prompt autocontenido:
```
Agent(subagent_type="fn-constructor", prompt=...)
```
Prompts en PARALELO en un mismo mensaje (varios Agent calls). Pasar:
- nombre propuesto, lang, domain
- firma esperada (params + return)
- pureza
- descripcion + ejemplo de uso (heredoc real detectado)
- nota: "esta funcion la necesita Claude para auto-uso futuro"
### 5. VALIDATE — ¿la proxima sesion la usara?
Despues de que fn-constructor termine:
```bash
./fn index 2>&1 | tail -2
# Verifica que las nuevas funciones existen
for fn in <lista>; do
mcp__registry__fn_show "$fn" >/dev/null && echo "OK: $fn" || echo "FAIL: $fn"
done
```
Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo:
```bash
cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose
```
Reporta:
- N funciones nuevas creadas (con IDs)
- N proposals nuevas en `registry.db.proposals`
- Recomendacion al usuario: "proximo turno mencionar/usar `<fn_id>` para validar que el wrapper se invoca correctamente"
### 6. SELF-TEST — telemetria del propio /fn_claude
`/fn_claude` mismo debe quedar registrado. Tras ejecutar, query final:
```bash
sqlite3 "$MON" "SELECT COUNT(*) FROM calls WHERE session_id = '${CLAUDE_SESSION_ID:-unknown}' AND ts >= <inicio_comando>"
```
Si la cuenta no aumento → el comando esta operando fuera de la telemetria (bug). Reportar.
---
## Reglas duras
1. **NO ejecutar fn-constructor para algo que ya existe.** Buscar primero via `mcp__registry__fn_search`. Si match relevante → NO crear duplicado.
2. **NO crear funciones especulativas.** Cada candidato debe tener evidencia real (snippet de heredoc o llamada inline detectada en esta sesion o en `call_monitor.calls` reciente).
3. **PARALELO**: si hay >1 candidato, lanza todos los `fn-constructor` en un solo mensaje con multiples `Agent` calls. NO secuencial.
4. **No autonomous merge**: las funciones nuevas viven en el branch local. NO push automatico. Humano revisa y push manual.
5. **Limites duros**: max 5 funciones nuevas por invocacion. Si detectas mas, prioriza por evidence weight (`occurrences * recency`) y reporta el resto como pending.
6. **Si la sesion no esta siendo registrada (`calls_session = 0`)**: ABORT antes de fase 2. No tiene sentido auto-construir sin telemetria.
---
## Output canonico
```
=== /fn_claude — auto-auditoria ===
session_id: <id>
calls_session: N
calls_24h: M (mcp_ratio: 0.XX)
violations_24h: K
pending_proposals: P (existentes en registry.db)
GAPS DETECTADOS:
1. <name>_<lang>_<domain> — razon — evidencia
2. ...
LANZADOS (en paralelo):
fn-constructor #1: <name1> → en progreso
fn-constructor #2: <name2> → en progreso
...
VALIDADAS tras ./fn index:
✓ <name1>_<lang>_<domain>
✓ <name2>_<lang>_<domain>
PROPOSALS NUEVAS: <count>
PROXIMO TURNO: menciona `<name1>` para validar wrapper.
```
---
## Cuando usar
- Al inicio de una sesion larga, para verificar telemetria activa.
- A media sesion, cuando notes que estas reescribiendo el mismo bloque.
- Antes de cerrar sesion, para capitalizar lo aprendido como funciones reutilizables.
- Tras `/autonomous-task` para validar que el orquestador no genero ruido (proposals/funciones huerfanas).
---
## Cuando NO usar
- En sesiones cortas (<5 turnos) — no hay datos suficientes.
- Si `call_monitor.operations.db` no esta inicializado (`call_monitor init` primero).
- Si el usuario quiere control manual del proceso de extraccion. Este comando es agresivo.
+486
View File
@@ -0,0 +1,486 @@
# /frontend — Skill para proyectos frontend
Eres un arquitecto frontend experto. Esta skill se activa cuando el usuario pide crear un proyecto frontend, una app con UI, un componente nuevo, o una feature frontend. Tu trabajo es garantizar que TODO el frontend se construya usando el sistema de funciones reutilizables del registry y las mejores practicas actuales.
## Stack
- **pnpm** — gestor de paquetes
- **React 19** — UI library
- **Vite 8** — build tool
- **Mantine v9** — component library + styling (props, no CSS manual)
- **Phosphor Icons** — `@phosphor-icons/react`
- **Recharts** — charts (via `@mantine/charts`)
**NO usar:** Tailwind, shadcn, CVA, clsx, cn(), lucide-react, styled-components, emotion, CSS-in-JS runtime.
---
## PASO 1: Consultar el registry (OBLIGATORIO)
Antes de escribir una sola linea de codigo, consulta registry.db para saber que componentes, funciones y tipos frontend ya existen:
```bash
# Componentes y funciones frontend disponibles
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
# Tipos frontend disponibles
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
# Busqueda FTS5 si buscas algo especifico
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:chart* OR description:chart*') ORDER BY name;"
```
Tambien lista los archivos reales en disco ya que no todos estan indexados aun:
```bash
ls frontend/functions/ui/ # Componentes React
ls frontend/functions/core/ # Utilidades TS puras
ls frontend/types/ # Tipos
```
**REGLA:** Si un componente ya existe en `frontend/functions/ui/` (alias `@fn_library`), USALO. Nunca recrear lo que ya existe.
---
## PASO 2: Determinar el tipo de trabajo
### A) App nueva en `apps/`
Ir a → Seccion SCAFFOLD APP
### B) Componente nuevo para el registry
Ir a → Seccion CREAR COMPONENTE
### C) Feature en app existente
Ir a → Seccion CREAR FEATURE
---
## SCAFFOLD APP
Crear la estructura completa de una app frontend nueva en `apps/{nombre}/frontend/`.
### Estructura obligatoria
```
apps/{nombre}/
frontend/
package.json
vite.config.ts
tsconfig.json
postcss.config.cjs
index.html
src/
main.tsx # Entry point con MantineProvider
App.tsx # Root con Router
app.css # Minimal (font-smoothing solo)
features/ # Feature-based co-location
{feature}/
components/ # Componentes del feature
hooks/ # Hooks del feature
types.ts # Tipos del feature
index.ts # Barrel export publico
components/ # Componentes compartidos de esta app (no reutilizables)
hooks/ # Hooks compartidos
lib/ # Utilidades, API client
types/ # Tipos globales de la app
```
### package.json base
```json
{
"name": "{nombre}",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "tsc -b && vite build",
"preview": "vite preview --host"
},
"dependencies": {
"@mantine/core": "^9.0.0",
"@mantine/hooks": "^9.0.0",
"@mantine/notifications": "^9.0.0",
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "~5.9.3",
"vite": "^8.0.0"
}
}
```
Agregar dependencias extras segun necesidad:
- **Charts**: `@mantine/charts`, `recharts`
- **Tablas**: `@tanstack/react-table`
- **Forms**: `react-hook-form`, `@hookform/resolvers`, `zod`
- **Dates**: `@mantine/dates`, `dayjs`
- **Router**: `react-router` o `@tanstack/react-router`
- **State**: `zustand` (client state), `@tanstack/react-query` (server state)
- **Wails**: los hooks de Wails ya estan en `@fn_library` (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent, WailsProvider)
### vite.config.ts base
```ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@fn_library': resolve(__dirname, '../../../frontend/functions/ui'),
},
dedupe: ['react', 'react-dom'],
},
css: {
postcss: resolve(__dirname, './postcss.config.cjs'),
},
build: {
target: 'es2022',
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
},
},
},
},
})
```
### postcss.config.cjs base
```js
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
```
### app.css base
```css
/* Minimal — Mantine handles all theming via MantineProvider */
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
### main.tsx base
```tsx
import '@mantine/core/styles.css'
import '@mantine/notifications/styles.css'
import './app.css'
import React from 'react'
import ReactDOM from 'react-dom/client'
import { MantineProvider, createTheme } from '@mantine/core'
import { Notifications } from '@mantine/notifications'
import App from './App'
const theme = createTheme({
primaryColor: 'blue',
defaultRadius: 'md',
// Customize colors, fonts, etc. here
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MantineProvider theme={theme} defaultColorScheme="dark">
<Notifications />
<App />
</MantineProvider>
</React.StrictMode>,
)
```
### Despues del scaffold
```bash
cd apps/{nombre}/frontend && pnpm install
```
---
## CREAR COMPONENTE
Para componentes nuevos que van al registry en `frontend/functions/`.
### Reglas de implementacion
1. **Mantine first**: wrappear componentes de Mantine. Solo crear desde cero si Mantine no tiene equivalente.
2. **Styling via props**: usar props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.) y el style system. NUNCA clases CSS manuales ni Tailwind.
3. **CSS variables de Mantine**: si necesitas styles inline, usar `var(--mantine-color-*)`, `var(--mantine-spacing-*)`, etc.
4. **Iconos**: usar `@phosphor-icons/react`, no lucide-react ni @tabler/icons-react.
5. **Props tipadas**: usar `React.ComponentPropsWithoutRef<"element">` para HTML props spreading.
6. **Accesibilidad**:
- Elementos semanticos: `<button>` para acciones, `<a>` para navegacion
- NUNCA `<div onClick>` para elementos interactivos
- `aria-label` en botones de solo icono
- `aria-invalid` + `aria-describedby` en inputs con error
- Focus management en modales/popovers
7. **Discriminated unions** cuando las props cambian segun variante:
```tsx
type Props = { size?: 'sm' | 'md' | 'lg'; children: React.ReactNode } & (
| { variant: 'link'; href: string; onClick?: never }
| { variant: 'button'; onClick: () => void; href?: never }
)
```
### Patron de archivo .tsx
```tsx
import { Select, type SelectProps } from '@mantine/core'
// Re-export con defaults o logica adicional si necesario
interface MySelectProps extends Omit<SelectProps, 'xxx'> {
customProp?: string
}
function MySelect({ customProp, ...props }: MySelectProps) {
return <Select {...props} />
}
export { MySelect }
export type { MySelectProps }
```
### Patron de archivo .md
**IMPORTANTE:** El campo `lang` debe ser `ts` (no `typescript`). El indexer solo reconoce `ts`. Los IDs siguen el formato `{name}_ts_{domain}`.
```yaml
---
name: component_name
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "ComponentName(props: ComponentProps): JSX.Element"
description: "Descripcion concisa de que hace el componente"
tags: [component, ui, ...]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@mantine/core"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/component_name.tsx"
props:
- name: variant
type: "'default' | 'secondary'"
required: false
description: "Estilo visual"
emits: []
has_state: false
framework: react
variant: [default]
---
## Ejemplo
...codigo de ejemplo...
## Notas
...notas relevantes...
```
### Despues de crear
```bash
./fn index && ./fn show {id}
```
---
## CREAR FEATURE
Para features dentro de una app existente. Co-location obligatoria.
### Estructura
```
src/features/{feature_name}/
components/
FeatureMain.tsx # Componente principal
FeatureDetail.tsx # Sub-componentes
hooks/
useFeatureData.ts # Hooks del feature
types.ts # Tipos locales
index.ts # Barrel export
```
### Barrel export (index.ts)
```ts
// Solo exportar la API publica del feature
export { FeatureMain } from './components/FeatureMain'
export { useFeatureData } from './hooks/useFeatureData'
export type { FeatureItem, FeatureConfig } from './types'
```
### Patrones de estado obligatorios
**Server state** (datos de API/backend):
```tsx
// Con @tanstack/react-query
const queryKeys = {
all: ['feature'] as const,
list: (filters: Filters) => [...queryKeys.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.all, 'detail', id] as const,
}
function useFeatureList(filters: Filters) {
return useQuery({
queryKey: queryKeys.list(filters),
queryFn: () => fetchFeatureList(filters),
})
}
```
**Client state** (UI state compartido):
```tsx
// Con Zustand
import { create } from 'zustand'
interface FeatureStore {
selectedId: string | null
setSelected: (id: string | null) => void
}
const useFeatureStore = create<FeatureStore>((set) => ({
selectedId: null,
setSelected: (id) => set({ selectedId: id }),
}))
```
**Wails** (apps de escritorio):
```tsx
// Usar hooks del registry
import { useWailsQuery, useWailsMutation } from '@fn_library'
function useFeatureData() {
return useWailsQuery('GetFeatureData', [], { staleTime: 60_000 })
}
```
### Code splitting por ruta
```tsx
import { lazy, Suspense } from 'react'
import { Skeleton } from '@mantine/core'
const FeaturePage = lazy(() => import('./features/feature/components/FeaturePage'))
function AppRoutes() {
return (
<Routes>
<Route path="/feature" element={
<Suspense fallback={<Skeleton height="100vh" />}>
<FeaturePage />
</Suspense>
} />
</Routes>
)
}
```
---
## CHECKLIST DE VALIDACION (ejecutar siempre al final)
Antes de dar por terminado cualquier trabajo frontend, verificar:
### Colores y estilos
- [ ] CERO colores hardcodeados en componentes (no hex, no rgb inline)
- [ ] Styling via props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.)
- [ ] Si se necesitan styles inline, usar CSS variables de Mantine (`var(--mantine-color-*)`)
- [ ] NO clases CSS manuales, NO Tailwind, NO cn(), NO CVA
### Componentes del registry
- [ ] Verificado que no se esta recreando algo que ya existe en `@fn_library` (`frontend/functions/ui/`)
- [ ] Componentes de `@fn_library` usados donde aplica: Card, Select, SimpleSelect, KPICard, Sparkline, DashboardLayout, DataTable, charts, hooks Wails
- [ ] Componentes de Mantine usados directamente donde `@fn_library` no tiene wrapper: Button, TextInput, Table, Alert, Badge, Skeleton, Tabs, Tooltip, Group, Stack, Grid, Box, Paper, AppShell, Container
### Iconos
- [ ] Usando `@phosphor-icons/react` para iconos
- [ ] NO lucide-react, NO @tabler/icons-react
### TypeScript
- [ ] Props interfaces con `React.ComponentPropsWithoutRef` para HTML spreading
- [ ] Discriminated unions donde las props varian segun tipo/variante
- [ ] `as const` para arrays literales y config objects
- [ ] No `any` — usar `unknown` + type guards si es necesario
### Accesibilidad
- [ ] Elementos semanticos (button, a — no div onClick)
- [ ] `aria-label` en botones de solo icono
- [ ] `aria-invalid` + `aria-describedby` en inputs con validacion
- [ ] Focus trap en modales y popovers
- [ ] `prefers-reduced-motion` respetado (ya en app.css base)
### Performance
- [ ] Lazy loading en rutas (`React.lazy` + `Suspense`)
- [ ] `manualChunks` en vite.config para vendor splitting
- [ ] Sin barrel exports profundos que maten tree-shaking
- [ ] Listas largas virtualizadas si >100 items
### Estructura
- [ ] Features co-located: componente + hook + tipos + barrel en el mismo directorio
- [ ] Un `index.ts` por feature con API publica explicita
- [ ] Componentes reutilizables de la app en `src/components/`
- [ ] Tipos compartidos en `src/types/`
---
## ANTI-PATRONES (nunca hacer)
1. **`<div onClick={...}>`** → usar `<button>` o componente Mantine
2. **`style={{ color: '#3b82f6' }}`** → usar prop `c="blue"` o `var(--mantine-color-blue-6)`
3. **`import Button from './MyButton'`** cuando existe en Mantine → usar `import { Button } from '@mantine/core'`
4. **Estado global para todo** → segmentar: server state (React Query), client state (Zustand), form state (React Hook Form), URL state (search params)
5. **`index.ts` en la raiz de `src/`** que re-exporta todo → mata tree-shaking
6. **`// @ts-ignore`** → arreglar el tipo
7. **CSS-in-JS runtime** (styled-components, emotion) → usar props de Mantine
8. **Tailwind, CVA, cn(), clsx** → usar props de Mantine y su style system
9. **Crear utilidades que ya existen**: `getSeriesColor()`, `ChartContainer`, `DashboardLayout`, `DataTable` ya estan en `@fn_library`
10. **Colores de chart hardcodeados** → usar `@mantine/charts` color system o `getSeriesColor()`
$ARGUMENTS
+38
View File
@@ -0,0 +1,38 @@
# /full-git-pull — Pull automático de fn_registry + sub-repos + submodules + fn sync
Wrapper sobre el pipeline `full_git_pull_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
```bash
cd /home/lucas/fn_registry
./fn run full_git_pull_bash_pipelines
```
## Argumento
`$ARGUMENTS` — sin uso, ignorar.
## Qué hace el pipeline
1. `discover_git_repos_bash_infra` — lista repos locales (mismas exclusiones que push).
2. `git_pull_with_stash_bash_infra` por repo: stash si dirty → fetch → pull --ff-only → pop. Estados posibles por repo: `[pulled]`, `[up-to-date]`, `[diverged]`, `[stash-conflict]`.
3. `git submodule update --init --recursive` en root.
4. `git_pull_with_stash` sobre `~/.password-store`.
5. `CGO_ENABLED=1 ./fn index` para regenerar `registry.db`.
6. `./fn sync` con credenciales de `pass`.
## Notas
- **Modo no-interactivo.** Auto-stash con `--include-untracked`.
- **Fast-forward + merge auto.** Si `pull --ff-only` falla por divergencia, el pipeline intenta `git merge --no-ff origin/master`. Si el merge se aplica sin conflictos lo conserva como `[merged-auto]`. Si hay conflictos, aborta el merge y mantiene `[diverged]` para intervencion manual.
- **No clona repos faltantes.** Cada PC tiene su subset. Para añadir uno, clonarlo a mano y mirar `pc_locations` para reproducir el path.
- Para tocar la lógica: editar las funciones del registry, no este wrapper.
## Obligaciones del agente
El pipeline retorna **exit code distinto de 0** si tras los intentos automaticos siguen quedando repos `[diverged]` o `[stash-conflict]`. En ese caso el agente DEBE:
1. Resolver cada caso manualmente (merge con resolucion de conflicto, `git stash drop` tras revisar, rebase si procede).
2. Volver a ejecutar `/full-git-pull` hasta salida limpia.
3. Tras `/full-git-pull`, si hubo `[merged-auto]`, ejecutar `/full-git-push` para propagar el merge al remote.
Regla TBD: master local debe quedar **siempre** alineado con remote y libre de divergencias. Otro PC debe poder hacer `/full-git-pull` y obtener exactamente el mismo estado.
+41
View File
@@ -0,0 +1,41 @@
# /full-git-push — Push automático de fn_registry + sub-repos + fn sync
Wrapper sobre el pipeline `full_git_push_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
```bash
cd /home/lucas/fn_registry
./fn run full_git_push_bash_pipelines "$ARGUMENTS"
```
## Argumento
`$ARGUMENTS` — opcional. Mensaje de commit fijo para todos los repos dirty. Sin argumento, el pipeline genera un mensaje automático por repo según los paths cambiados (ver `bash/functions/infra/git_auto_commit_dirty.sh`).
## Qué hace el pipeline
1. `discover_git_repos_bash_infra` — lista repos bajo `fn_registry` (excluye `node_modules`, `.venv`, `cpp/vendor`, `cpp/build`, `sources`, `temp`, `subrepos`).
2. Auto-inicializa apps/analyses sin `.git` con `ensure_repo_synced_bash_infra` (Gitea `dataforge/<basename>`).
3. `scan_secrets_in_dirty_bash_cybersecurity` — aborta si detecta nombres sospechosos (`.env*`, `*credentials*`, `*.key`, `*.pem`, `id_rsa*`, `*secret*`, `*token*.txt`).
4. `git_auto_commit_dirty_bash_infra` — commitea cada repo dirty.
5. `git_push_if_ahead_bash_infra` — push solo si `rev-list @{u}..HEAD > 0` (sin red previa).
6. Push de `~/.password-store` (sin commitear, pass autocommitea).
7. `./fn sync` con credenciales cargadas desde `pass`.
## Notas
- **Modo no-interactivo por diseño.** Auto-commitea sin preguntar.
- **Único motivo de aborto antes de commitear:** secret detectado por nombre.
- Si un pre-commit hook bloquea (ej. `audit_uses_functions` con drift), el pipeline reintenta con `--no-verify` para no perder cambios. Los bypasses se reportan en bloque `[!] Hook bypasses` al final.
- Si un push es rechazado por non-fast-forward, el pipeline intenta `git merge --no-ff origin/master` automaticamente y vuelve a pushear. Si el merge tiene conflictos, lo aborta y reporta.
- Para tocar la lógica: editar las funciones del registry, no este wrapper.
## Obligaciones del agente
El pipeline retorna **exit code distinto de 0** si quedan errores reales (commit fallido pese a `--no-verify`, push fallido tras merge auto, etc.) y los lista bajo `[!!] ERRORES`. Cuando esto ocurra el agente DEBE:
1. Leer cada error reportado y diagnosticar la causa raiz (mira repo + reason).
2. Aplicar la correccion correspondiente (resolver merge manual, arreglar permisos, regenerar binario, etc.).
3. Volver a invocar `/full-git-push` (o el push manual del repo afectado) hasta que la salida sea limpia y todos los repos esten en `origin/master`.
4. Si aparece bloque `[!] Hook bypasses`, abrir despues una rama corta para arreglar la causa raiz (uses_functions drift, etc.) y commitear con hooks activos. No es bloqueante para el push pero es deuda a saldar pronto.
Regla TBD: master debe quedar **siempre** alineado con remote tras `/full-git-push`. Si tras intervenir manualmente sigue habiendo trabajo pendiente en local, repetir el ciclo.
+747
View File
@@ -0,0 +1,747 @@
# /meta_bigq — Operar Metabase y BigQuery desde el registry
Eres un agente de datos. Tienes acceso a funciones Python del fn_registry para controlar **Metabase** (dashboards, cards, queries, usuarios) y **Google BigQuery** (datasets, tablas, queries, jobs, routines). Usa estas funciones directamente — no inventes llamadas HTTP manuales.
---
## Como ejecutar funciones
```bash
PYTHON="python/.venv/bin/python3"
# Ejecutar codigo inline
$PYTHON -c "
import sys; sys.path.insert(0, 'python/functions')
from metabase import metabase_auth, metabase_list_dashboards
client = metabase_auth('http://localhost:3000', 'admin@fnregistry.local', 'FnRegistry2024!')
print(metabase_list_dashboards(client))
"
# O con fn run para pipelines
./fn run init_metabase --project fn_registry
./fn run setup_metabase_volume
./fn run metabase_create_ops_dashboard docker_tui
```
Variables de entorno tipicas:
- `METABASE_URL` (default: `http://localhost:3000`)
- `METABASE_ADMIN_EMAIL` (default: `admin@fnregistry.local`)
- `METABASE_ADMIN_PASSWORD` (default: `FnRegistry2024!`)
- BigQuery usa ADC (`gcloud auth application-default login`) o `GOOGLE_APPLICATION_CREDENTIALS`
---
## METABASE — Referencia rapida
### Auth
```python
from metabase import metabase_auth, MetabaseClient
# Login con email/password
client = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
# O directo con API key
client = MetabaseClient("http://localhost:3000", "mb_api_key_xxxxx")
# Context manager
with metabase_auth(...) as client:
pass # se cierra solo
```
### Cards (preguntas)
```python
from metabase import (
metabase_list_cards, # (client, filter="", model_id=0) -> list[dict]
metabase_get_card, # (client, card_id) -> dict
metabase_create_card, # (client, name, dataset_query, display="table", collection_id=0, description="") -> dict
metabase_update_card, # (client, card_id, **fields) -> dict # fields: name, description, display, dataset_query, archived...
metabase_delete_card, # (client, card_id) -> None # IRREVERSIBLE, preferir archived=True
metabase_execute_card, # (client, card_id, parameters=None) -> dict # ejecuta query de card guardada
metabase_execute_query, # (client, database_id, sql, max_results=0) -> dict # query ad-hoc
)
# Crear card con SQL nativo
card = metabase_create_card(client, "Ventas por mes", {
"database": 1, "type": "native",
"native": {"query": "SELECT date_trunc('month', created_at) as mes, SUM(total) FROM orders GROUP BY 1"},
}, display="line")
# Actualizar query de una card
metabase_update_card(client, card["id"], dataset_query={
"database": 1, "type": "native",
"native": {"query": "SELECT ... nueva query ..."},
})
# Archivar (soft-delete)
metabase_update_card(client, 42, archived=True)
# Query ad-hoc sin guardar
result = metabase_execute_query(client, 1, "SELECT COUNT(*) FROM users")
# result["data"]["rows"] = [[42]]
```
**Filtros de list_cards:** `all`, `mine`, `fav`, `archived`, `recent`, `popular`, `database`, `table`
### Dashboards
```python
from metabase import (
metabase_list_dashboards, # (client, filter="") -> list[dict]
metabase_get_dashboard, # (client, dashboard_id) -> dict # incluye dashcards
metabase_create_dashboard, # (client, name, description="", collection_id=0) -> dict
metabase_update_dashboard, # (client, dashboard_id, **fields) -> dict
metabase_delete_dashboard, # (client, dashboard_id) -> None # IRREVERSIBLE
)
# Crear dashboard + agregar cards
dash = metabase_create_dashboard(client, "KPIs Operativos", description="Metricas diarias")
# Posicionar cards en el dashboard (dashcards es el estado COMPLETO)
metabase_update_dashboard(client, dash["id"], dashcards=[
{"id": -1, "card_id": card1["id"], "row": 0, "col": 0, "size_x": 6, "size_y": 4},
{"id": -2, "card_id": card2["id"], "row": 0, "col": 6, "size_x": 6, "size_y": 4},
{"id": -3, "card_id": card3["id"], "row": 4, "col": 0, "size_x": 12, "size_y": 6},
])
# id negativo = card nueva, id positivo = card existente, omitida = eliminada
```
**Filtros de list_dashboards:** `all`, `mine`, `archived`
### Dashboards — helpers compositivos (añadir KPIs a dashboard existente)
Helpers para el flujo tipico "anadir N cards (KPI) al final de un tab existente reusando los mismos filtros que otro card vecino". Evitan los gotchas: replicar `parameter_mappings`, calcular `row` libre, escapado raro de `column_settings`, generacion de `lib/uuid` en MBQL.
```python
from metabase import (
metabase_mbql_from_source_card,
metabase_copy_dashcard_mappings,
metabase_dashboard_next_row,
metabase_dashboard_append_row,
metabase_viz_column_format,
metabase_smartscalar_anothercolumn_viz,
)
```
#### `metabase_mbql_from_source_card`
Construye `dataset_query` MBQL sobre una saved-card (`source-card`), con aggregations + joins + filters + breakouts + segunda stage de expressions. Genera `lib/uuid` automatico en cada nodo.
```python
dq = metabase_mbql_from_source_card(
database_id=6,
source_card_id=5305,
aggregations=[
{"op": "sum", "field": "PrecioVenta", "base_type": "type/Decimal"},
{"op": "sum", "field": "PrecioCompra", "base_type": "type/Decimal"},
{"op": "sum", "field": "PrecioTasas", "base_type": "type/Float"},
],
joins=[
{"alias": "Centros - idCentro", "source_card_id": 4076,
"fields": "none", "local_field": "idCentro", "local_base_type": "type/Text",
"foreign_field_id": 17316, "foreign_base_type": "type/Text"},
],
filters=[["not-empty", {}, ["field", {"base-type": "type/Text"},
"Centros - idCentro__Companies__name"]]],
expressions=[
{"name": "MasadeMargen", "expr":
{"op": "-", "args": [{"field": "sum"},
{"op": "+", "args": [{"field": "sum_2"}, {"field": "sum_3", "base_type": "type/Float"}]}]}},
{"name": "Margen", "expr":
{"op": "coalesce", "args": [
{"op": "/", "args": [
{"op": "-", "args": [{"field": "sum"},
{"op": "+", "args": [{"field": "sum_2"}, {"field": "sum_3", "base_type": "type/Float"}]}]},
{"field": "sum"}]},
0]}},
],
)
```
Ops soportadas en expressions: `+`, `-`, `*`, `/`, `coalesce`, `case`. Referencia a otra expresion en la misma stage: `{"ref": "Margen"}`. Aliases de aggregations son posicionales: `sum`, `sum_2`, `sum_3`... (orden = declaracion).
#### `metabase_copy_dashcard_mappings`
Copia los `parameter_mappings` de un dashcard "donante" a un card nuevo. Devuelve lista lista para pegar en `dashcards_add`.
```python
mappings = metabase_copy_dashcard_mappings(
client,
dashboard_id=734,
source_card_id=9918, # card donante con 18 filtros mapeados
dest_card_id=9947, # card destino nueva
)
# Devuelve [{"parameter_id","card_id","target"}, ...] con card_id=9947
```
#### `metabase_dashboard_next_row`
Calcula el primer `row` libre al final de un tab.
```python
row = metabase_dashboard_next_row(client, dashboard_id=734, tab_id=191)
# row=12 si el ultimo card termina en row+size_y=12
# tab_id=0 → dashboards sin tabs
```
#### `metabase_dashboard_append_row`
Combo: append N cards en una fila horizontal al final del tab, copiando mappings de un donante. Una sola llamada hace `next_row` + grid math + `copy_mappings` + `update_dashboard_safe`.
```python
metabase_dashboard_append_row(
client,
dashboard_id=734,
tab_id=191,
card_ids=[9947, 9948, 9949],
height=4,
donor_card_id=9918, # mismos 18 filtros del dashboard
grid_width=24, # default Metabase v0.59
)
# Coloca 3 cards de size_x=8 en row=next, cols 0/8/16, con mappings copiados
```
#### `metabase_viz_column_format`
Construye una entrada de `column_settings` con la clave JSON-escaped (`'["name","Margen"]'`) sin tener que recordar el formato exacto.
```python
metabase_viz_column_format("Margen", number_style="percent", decimals=2)
# {'["name","Margen"]': {"number_style": "percent", "decimals": 2}}
metabase_viz_column_format("MasadeMargen", number_style="currency",
currency="EUR", decimals=0, currency_in_header=False)
# {'["name","MasadeMargen"]': {...}}
```
Mergea varios resultados en `column_settings` de las visualization_settings.
#### `metabase_smartscalar_anothercolumn_viz`
Construye `visualization_settings` completo para `display=smartscalar` con comparativa tipo `anotherColumn` (compara dos columnas de la misma fila — no requiere breakout temporal).
```python
viz = metabase_smartscalar_anothercolumn_viz(
main_column="Margen",
compare_column="Margen_N1",
label="vs N-1",
number_style="percent",
decimals=2,
)
# Setear en /api/card via PUT visualization_settings=viz
```
**⚠ Gotcha smartscalar Metabase v0.59:** el visualization solo acepta `type: "anotherColumn"` cuando la query NO produce filas multiples. Si Metabase muestra el error *"Agrupa solo por un campo de tiempo para ver como ha cambiado con el tiempo"*, hace falta un **breakout temporal** en la MBQL (ej. `breakouts=[{"field":"fecha","base_type":"type/Date","temporal_unit":"month"}]`) y usar el comparison `previousValue` en lugar de `anotherColumn`. Alternativa: `metabase_smartscalar_kpi_sql` + `metabase_smartscalar_kpi_payload` (patron 2-row nativo) si la card es SQL nativo.
#### Patron canonico — anadir 3 KPI cards a tab existente
```python
import os, sys
sys.path.insert(0, "python/functions")
from metabase import (
MetabaseClient, metabase_create_card, metabase_mbql_from_source_card,
metabase_dashboard_append_row, metabase_viz_column_format,
metabase_smartscalar_anothercolumn_viz,
)
c = MetabaseClient("https://reports.autingo.es", os.environ["MB_API_KEY"])
# 1) MBQL reusando una saved-card como source
def query():
return metabase_mbql_from_source_card(
database_id=6, source_card_id=5305,
aggregations=[
{"op":"sum","field":"PrecioVenta","base_type":"type/Decimal"},
{"op":"sum","field":"PrecioCompra","base_type":"type/Decimal"},
{"op":"sum","field":"PrecioTasas","base_type":"type/Float"},
],
# joins/filters/expressions ...
)
# 2) Crear cards
card1 = metabase_create_card(c, "Masa de Margen", query(),
display="scalar", collection_id=500)
viz1 = {"scalar.field": "MasadeMargen",
"column_settings": metabase_viz_column_format(
"MasadeMargen", number_style="currency", currency="EUR", decimals=0)}
c._http.request("PUT", f"/api/card/{card1['id']}", json={"visualization_settings": viz1})
card2 = metabase_create_card(c, "Margen", query(), display="smartscalar", collection_id=500)
viz2 = metabase_smartscalar_anothercolumn_viz(
main_column="Margen", compare_column="Margen_N1", number_style="percent", decimals=2)
c._http.request("PUT", f"/api/card/{card2['id']}", json={"visualization_settings": viz2})
# 3) Append fila al tab con mappings copiados del donante
metabase_dashboard_append_row(
c, dashboard_id=734, tab_id=191,
card_ids=[card1["id"], card2["id"]],
height=4, donor_card_id=9918,
)
```
### Documents (ProseMirror)
Los "documents" son páginas narrativas editables con texto rico y cards embebidas. **No hay helpers en fn_registry todavía** — usa el endpoint REST directamente a través de `client._http`.
**Endpoints:**
| Método | Ruta | Qué hace |
|--------|------|---------|
| GET | `/api/document` | Lista documents (`{items: [...]}`) |
| GET | `/api/document/{id}` | Lee un document (incluye `document` con árbol ProseMirror) |
| POST | `/api/document` | Crea. Payload: `{name, collection_id, document}` |
| PUT | `/api/document/{id}` | Actualiza. Mismo payload que POST |
| PUT | `/api/document/{id}` con `{archived: true}` | Soft-delete |
```python
# Crear documento
resp = client._http.request("POST", "/api/document", json={
"name": "Mi análisis",
"collection_id": 583, # obligatorio — raíz no se acepta desde API
"document": {"type": "doc", "content": [
{"type": "heading", "attrs": {"level": 1}, "content": [{"type": "text", "text": "Título"}]},
{"type": "paragraph", "content": [{"type": "text", "text": "Cuerpo."}]},
]},
})
doc_id = resp.json()["id"]
print(f"https://reports.autingo.es/document/{doc_id}")
```
#### Tipos de nodo SOPORTADOS en Metabase v0.59.x
Solo estos tipos renderizan. **Cualquier tipo fuera de esta lista hace que el documento se vea vacío al abrirlo.**
```python
ALLOWED_DOC_NODES = {
"doc", "heading", "paragraph", "text",
"horizontalRule", "blockquote",
"bulletList", "listItem",
"codeBlock", # attrs.language ej: "sql"
"resizeNode", # envuelve SIEMPRE a cardEmbed
"cardEmbed", # solo dentro de resizeNode
}
```
Marcas inline válidas en nodos `text`: `bold`, `italic`, `code`, `strike` (se aplican con `"marks": [{"type": "bold"}, ...]`).
#### Tipos PROHIBIDOS (rompen el render)
- `table`, `tableRow`, `tableHeader`, `tableCell` → en v0.59.x no están registrados en el schema del editor y el doc entero se vuelve invisible.
- `callout` → idem (documentado en memoria `feedback_metabase_prosemirror.md`).
- `image`, `video`, `iframe`, `mention`, cualquier embed de terceros → no registrados.
Si necesitas una tabla, **emúlala con una `bulletList` de `**clave:** valor`**:
```python
def kv_list(pairs):
return {"type": "bulletList", "content": [
{"type": "listItem", "content": [
{"type": "paragraph", "content": [
{"type": "text", "text": k, "marks": [{"type": "bold"}]},
{"type": "text", "text": f": {v}"},
]},
]}
for k, v in pairs
]}
```
#### cardEmbed SIEMPRE dentro de resizeNode
Un `cardEmbed` suelto no renderiza. Patrón obligatorio:
```python
def card_embed(card_id, height=420):
import uuid
return {
"type": "resizeNode",
"attrs": {"height": height, "minHeight": 280},
"content": [{
"type": "cardEmbed",
"attrs": {"id": card_id, "name": None, "_id": str(uuid.uuid4())},
}],
}
```
#### Validación OBLIGATORIA antes de POST/PUT
Nunca envíes un document a Metabase sin validar primero. Un solo nodo prohibido lo deja invisible sin devolver error HTTP:
```python
ALLOWED = {"doc","heading","paragraph","text","horizontalRule","blockquote",
"bulletList","listItem","codeBlock","resizeNode","cardEmbed"}
def validate_doc(node, path=""):
errs = []
if isinstance(node, dict):
typ = node.get("type", "?")
if typ not in ALLOWED:
errs.append(f"{path}: tipo no permitido '{typ}'")
if typ == "resizeNode":
inner = node.get("content", [])
if not (len(inner) == 1 and inner[0].get("type") == "cardEmbed"):
errs.append(f"{path}: resizeNode debe contener exactamente un cardEmbed")
return errs # no re-descender al cardEmbed interno
for i, c in enumerate(node.get("content", []) or []):
errs += validate_doc(c, f"{path}/{typ}[{i}]")
return errs
errs = validate_doc(my_doc)
assert not errs, f"Doc inválido:\n" + "\n".join(f" - {e}" for e in errs)
```
#### Aprender estructura de un doc que ya funciona
Si dudas sobre un nodo, **clónalo de un doc existente que renderice**:
```python
d = client._http.request("GET", "/api/document/2").json()
# d["document"] contiene el árbol completo en ProseMirror
```
### Databases
```python
from metabase import (
metabase_list_databases, # (client, include_tables=False) -> list
metabase_add_database, # (client, name, engine, details) -> dict
metabase_get_database, # (client, database_id) -> dict
)
# Agregar SQLite
metabase_add_database(client, "Operations DB", "sqlite", {"db": "/data/operations.db"})
# Agregar PostgreSQL
metabase_add_database(client, "DW", "postgres", {
"host": "localhost", "port": 5432, "dbname": "warehouse",
"user": "reader", "password": "secret",
})
```
### Usuarios
```python
from metabase import (
metabase_list_users, # (client, status="", query="", limit=0, offset=0) -> dict
metabase_get_user, # (client, user_id) -> dict
metabase_create_user, # (client, first_name, last_name, email, password="", group_ids=None) -> dict
metabase_update_user, # (client, user_id, **fields) -> dict
metabase_deactivate_user, # (client, user_id) -> None # soft-delete
)
```
### Setup y pipelines
```python
from metabase import metabase_setup
# Setup inicial de instancia nueva (obtiene setup-token automaticamente)
metabase_setup("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
```
```bash
# Pipelines ejecutables con fn run
./fn run init_metabase --project fn_registry # Docker: Postgres + Metabase
./fn run setup_metabase_volume # Copiar registry.db al contenedor
./fn run metabase_add_ops_db docker_tui # Registrar operations.db como database
./fn run metabase_create_ops_dashboard docker_tui # Dashboard operativo completo
./fn run metabase_fix_permissions # Arreglar permisos SQLite en Docker
```
---
## BIGQUERY — Referencia rapida
### Auth
```python
from bigquery import bq_auth, BQClient
# ADC (gcloud auth application-default login)
client = bq_auth()
# Proyecto explicito
client = bq_auth("my-project-id")
# Service account JSON
client = bq_auth(credentials_path="/path/to/sa.json")
# Context manager
with bq_auth("my-project") as client:
pass
```
### Datasets
```python
from bigquery import (
bq_create_dataset, # (client, dataset_id, location="US", description="", labels=None, default_table_expiration_ms=0) -> dict
bq_get_dataset, # (client, dataset_id) -> dict
bq_list_datasets, # (client) -> list[dict]
bq_update_dataset, # (client, dataset_id, description=None, labels=None, default_table_expiration_ms=None) -> dict
bq_delete_dataset, # (client, dataset_id, delete_contents=False) -> None
)
bq_create_dataset(client, "analytics", location="EU", description="Data warehouse")
bq_delete_dataset(client, "temp", delete_contents=True) # borra tablas incluidas
```
### Tables
```python
from bigquery import (
bq_create_table, # (client, dataset_id, table_id, schema, partitioning=None, clustering=None, description="", labels=None) -> dict
bq_get_table, # (client, dataset_id, table_id) -> dict # schema, num_rows, num_bytes, partitioning...
bq_list_tables, # (client, dataset_id) -> list[dict]
bq_update_table, # (client, dataset_id, table_id, schema=None, description=None, labels=None) -> dict
bq_delete_table, # (client, dataset_id, table_id) -> None
bq_preview_rows, # (client, dataset_id, table_id, max_results=10) -> dict # SIN COSTE de query
)
# Crear tabla con particionamiento
bq_create_table(client, "analytics", "events",
schema=[
{"name": "event_id", "type": "STRING", "mode": "REQUIRED"},
{"name": "user_id", "type": "STRING"},
{"name": "event_type", "type": "STRING"},
{"name": "created_at", "type": "TIMESTAMP"},
{"name": "payload", "type": "JSON"},
],
partitioning={"type": "DAY", "field": "created_at"},
clustering=["event_type", "user_id"],
)
# Preview sin coste (usa Storage Read API, no ejecuta query)
preview = bq_preview_rows(client, "analytics", "events", max_results=5)
# {"columns": [...], "rows": [[...], ...], "total_rows": 1234567}
# Schema: solo se pueden AGREGAR columnas, nunca eliminar
bq_update_table(client, "analytics", "events", schema=[
*existing_schema,
{"name": "new_col", "type": "STRING"},
])
```
**Tipos de schema:** `STRING`, `INT64`, `FLOAT64`, `BOOL`, `TIMESTAMP`, `DATE`, `DATETIME`, `BYTES`, `NUMERIC`, `JSON`, `RECORD`/`STRUCT`, `GEOGRAPHY`
**Modos:** `NULLABLE` (default), `REQUIRED`, `REPEATED`
### Queries y datos
```python
from bigquery import (
bq_query, # (client, sql, params=None, dry_run=False) -> dict
bq_insert_rows, # (client, dataset_id, table_id, rows) -> dict
bq_load_from_gcs, # (client, uri, dataset_id, table_id, source_format="CSV", write_disposition="WRITE_APPEND", autodetect=True, skip_leading_rows=0) -> dict
bq_load_from_file, # (client, file_path, dataset_id, table_id, ...) -> dict # mismos params que gcs
bq_export_to_gcs, # (client, dataset_id, table_id, destination_uri, destination_format="CSV", compression="NONE") -> dict
bq_copy_table, # (client, source_dataset, source_table, dest_dataset, dest_table, write_disposition="WRITE_EMPTY") -> dict
)
# Query simple
result = bq_query(client, "SELECT COUNT(*) as total FROM analytics.events")
# {"columns": ["total"], "rows": [[1234567]], "total_rows": 1, "bytes_processed": 0, "cache_hit": True}
# Query parametrizada (usa @nombre en SQL)
result = bq_query(client, "SELECT * FROM analytics.events WHERE event_type = @tipo LIMIT @n", params=[
{"name": "tipo", "type": "STRING", "value": "purchase"},
{"name": "n", "type": "INT64", "value": 100},
])
# Estimar coste ANTES de ejecutar (no procesa datos)
estimate = bq_query(client, "SELECT * FROM analytics.events", dry_run=True)
# {"total_bytes_processed": 5368709120, "total_bytes_billed": 5368709120}
gb = estimate["total_bytes_processed"] / (1024**3)
print(f"Esta query procesara {gb:.2f} GB (~${gb * 6.25:.2f} USD)")
# Streaming insert
bq_insert_rows(client, "analytics", "events", [
{"event_id": "e1", "user_id": "u1", "event_type": "click", "created_at": "2026-04-07T10:00:00Z"},
{"event_id": "e2", "user_id": "u2", "event_type": "purchase", "created_at": "2026-04-07T10:01:00Z"},
])
# {"inserted": 2, "errors": []}
# Cargar CSV desde GCS
bq_load_from_gcs(client, "gs://bucket/data/*.csv", "analytics", "events",
source_format="CSV", write_disposition="WRITE_TRUNCATE", skip_leading_rows=1)
# Cargar archivo local
bq_load_from_file(client, "/tmp/data.parquet", "analytics", "events",
source_format="PARQUET", write_disposition="WRITE_APPEND")
# Exportar a GCS
bq_export_to_gcs(client, "analytics", "events", "gs://bucket/export/events-*.csv",
destination_format="CSV", compression="GZIP")
# Copiar tabla
bq_copy_table(client, "analytics", "events", "analytics_backup", "events_20260407")
```
**write_disposition:** `WRITE_TRUNCATE` (reemplazar), `WRITE_APPEND` (agregar), `WRITE_EMPTY` (solo si vacia)
**source_format:** `CSV`, `NEWLINE_DELIMITED_JSON`, `AVRO`, `PARQUET`, `ORC`
### Jobs
```python
from bigquery import (
bq_list_jobs, # (client, state_filter="", max_results=50, all_users=False) -> list[dict]
bq_get_job, # (client, job_id) -> dict # state, bytes_processed, errors
bq_cancel_job, # (client, job_id) -> dict
)
# Ver jobs corriendo
running = bq_list_jobs(client, state_filter="running")
for j in running:
print(j["job_id"], j["job_type"], j["bytes_processed"])
# Cancelar un job pesado
bq_cancel_job(client, "job_abc123")
```
**state_filter:** `running`, `pending`, `done`
### Routines (UDFs / Procedures)
```python
from bigquery import (
bq_create_routine, # (client, dataset_id, routine_id, body, routine_type="SCALAR_FUNCTION", language="SQL", arguments=None, return_type="", description="") -> dict
bq_list_routines, # (client, dataset_id) -> list[dict]
bq_delete_routine, # (client, dataset_id, routine_id) -> None
)
# UDF SQL
bq_create_routine(client, "analytics", "double_value",
body="x * 2",
arguments=[{"name": "x", "data_type": "INT64"}],
return_type="INT64",
)
# Stored procedure
bq_create_routine(client, "analytics", "refresh_summary",
body="BEGIN INSERT INTO summary SELECT ... FROM events; END;",
routine_type="PROCEDURE",
)
# UDF JavaScript
bq_create_routine(client, "analytics", "parse_ua",
body="return uaParser.parse(ua).browser.name;",
language="JAVASCRIPT",
arguments=[{"name": "ua", "data_type": "STRING"}],
return_type="STRING",
)
```
---
## Flujos tipicos
### 1. Explorar BigQuery y visualizar en Metabase
```python
import sys; sys.path.insert(0, "python/functions")
from bigquery import bq_auth, bq_query
from metabase import metabase_auth, metabase_create_card, metabase_create_dashboard, metabase_update_dashboard
# 1. Explorar datos en BQ
bq = bq_auth("my-project")
result = bq_query(bq, "SELECT event_type, COUNT(*) as cnt FROM analytics.events GROUP BY 1 ORDER BY 2 DESC LIMIT 10")
print(result["columns"], result["rows"])
# 2. Registrar BQ como database en Metabase (si no esta)
# Metabase soporta BigQuery como engine nativo
# 3. Crear cards en Metabase apuntando a BQ
mb = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
card = metabase_create_card(mb, "Eventos por tipo", {
"database": 2, # ID de la database BQ en Metabase
"type": "native",
"native": {"query": "SELECT event_type, COUNT(*) as cnt FROM analytics.events GROUP BY 1 ORDER BY 2 DESC"},
}, display="bar")
# 4. Crear dashboard
dash = metabase_create_dashboard(mb, "Analytics Overview")
metabase_update_dashboard(mb, dash["id"], dashcards=[
{"id": -1, "card_id": card["id"], "row": 0, "col": 0, "size_x": 12, "size_y": 6},
])
```
### 2. ETL: archivo local -> BigQuery -> Metabase dashboard
```python
from bigquery import bq_auth, bq_load_from_file, bq_query, bq_preview_rows
from metabase import metabase_auth, metabase_execute_query
bq = bq_auth("my-project")
# Cargar datos
bq_load_from_file(bq, "/tmp/sales.csv", "warehouse", "sales",
source_format="CSV", write_disposition="WRITE_TRUNCATE", skip_leading_rows=1)
# Verificar
preview = bq_preview_rows(bq, "warehouse", "sales", max_results=3)
print(preview["total_rows"], "filas cargadas")
# Consultar via Metabase (si BQ esta registrado como database)
mb = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
result = metabase_execute_query(mb, 2, "SELECT region, SUM(amount) FROM sales GROUP BY 1")
```
### 3. Montar infraestructura desde cero
```bash
# 1. Levantar Metabase + Postgres
./fn run init_metabase --project fn_registry
# 2. Copiar registry.db al contenedor
./fn run setup_metabase_volume
# 3. Setup inicial
python/.venv/bin/python3 -c "
import sys; sys.path.insert(0, 'python/functions')
from metabase import metabase_setup
metabase_setup('http://localhost:3000', 'admin@fnregistry.local', 'FnRegistry2024!')
"
# 4. Registrar operations.db de una app
./fn run metabase_add_ops_db docker_tui
# 5. Dashboard operativo automatico
./fn run metabase_create_ops_dashboard docker_tui
```
### 4. Auditar costes de BigQuery
```python
from bigquery import bq_auth, bq_list_jobs, bq_query
bq = bq_auth("my-project")
# Jobs recientes completados
jobs = bq_list_jobs(bq, state_filter="done", max_results=20, all_users=True)
total_bytes = sum(j.get("bytes_processed") or 0 for j in jobs)
print(f"Ultimos 20 jobs: {total_bytes / (1024**3):.2f} GB procesados")
# Dry-run antes de queries caras
estimate = bq_query(bq, "SELECT * FROM analytics.events WHERE created_at > '2026-01-01'", dry_run=True)
gb = estimate["total_bytes_processed"] / (1024**3)
cost = gb * 6.25 # $6.25/TB on-demand
print(f"Coste estimado: ${cost:.2f} USD ({gb:.1f} GB)")
```
---
## Buscar mas funciones
Si necesitas algo que no esta aqui, busca en el registry:
```bash
# FTS5 por nombre o descripcion
./fn search "lo que buscas"
# Ver detalles de una funcion
./fn show <id>
# Inline desde Python
sqlite3 registry.db "SELECT id, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:export*') ORDER BY name;"
```
$ARGUMENTS
+53
View File
@@ -0,0 +1,53 @@
# /new-cpp-app — Crear app C++ nueva con scaffolder estandar
Wrapper sobre el pipeline `init_cpp_app_bash_pipelines`. Genera la estructura canonica que cumple `cpp/PATTERNS.md` y `.claude/rules/cpp_apps.md` (main.cpp con `cfg.about/log/panels`, sin `app_menubar` manual, dockspace via framework), registra la app en `cpp/CMakeLists.txt`, crea repo Gitea `dataforge/<name>` y ejecuta `fn index`.
```bash
cd /home/lucas/fn_registry
./fn run init_cpp_app $ARGUMENTS
```
## Uso
```
/new-cpp-app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"]
```
## Ejemplos
```bash
# App suelta en cpp/apps/<name>/
/new-cpp-app my_tool --desc "Herramienta para X"
# App dentro de un proyecto
/new-cpp-app finance_panel --project budget --desc "Panel de finanzas" --tags "finance,dashboard"
```
## Que genera
```
<dir>/
main.cpp # Plantilla canonica: panels[] + cfg.about + cfg.log + run_app(cfg, render)
CMakeLists.txt # add_imgui_app(<name> main.cpp)
app.md # Frontmatter completo (lang:cpp, framework:imgui, dir_path, repo_url)
```
Mas registro en `cpp/CMakeLists.txt`, repo Gitea con commit inicial, y `fn index` para que aparezca en `registry.db`.
## Despues de crear
1. Editar `app.md` y completar `uses_functions` cuando la app consuma funciones del registry.
2. Anadir las funciones al `CMakeLists.txt` como paths absolutos: `${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp`.
3. Build: `/compile <name>` o `cd cpp && cmake --build build --target <name> -j`.
## Cuando NO usar
NUNCA — esta es la unica via para crear apps C++ nuevas. Si el scaffolder no cubre un caso, modificar la plantilla en `bash/functions/pipelines/init_cpp_app.sh`. Escribir `main.cpp + CMakeLists.txt + app.md` a mano esta prohibido por `.claude/rules/cpp_apps.md`.
## Auditoria post-creacion
```
fn doctor cpp-apps
```
Lista apps que se desvian del estandar (sin `cfg.about`, con `app_menubar` manual, dockspace duplicado, etc.).
+97
View File
@@ -0,0 +1,97 @@
---
description: "Recordatorio operativo para usar subagentes fn (constructor/executor/recopilador/analizador/mejorador) y paralelizar trabajo independiente"
---
# /subagentes — usa subagentes fn y paraleliza
Recuerda: antes de escribir codigo nuevo o ejecutar pipelines en serie, **delega a subagentes** y **paraleliza** llamadas independientes (un mensaje, varios `Agent` calls).
---
## Mapa de subagentes fn (ciclo reactivo)
| Fase | Agente | Cuando dispararlo |
|---|---|---|
| 1 CONSTRUIR | `fn-constructor` | Falta funcion/tipo/test reutilizable. NUNCA escribir inline en `apps/` si es reutilizable |
| 2 EJECUTAR | `fn-executor` | Correr pipeline/funcion del registry + registrar ejecucion en `operations.db` |
| 3 RECOPILAR | `fn-recopilador` | Auditar integridad de `operations.db`. Modo `design-e2e <app>` propone bloque `e2e_checks` |
| 4 ANALIZAR | `fn-analizador` | Ejecutar `e2e_checks` de `app.md`, veredicto pass/fail, persistir en `e2e_runs` |
| 5 MEJORAR | `fn-mejorador` | Convertir fallos de `e2e_runs` en `proposals` con evidencia trazable |
| 6 META | `fn-orquestador` | Recorrer fases 1-5 solo hasta convergencia. Sandbox `auto/<issue>`. Issue 0069 |
**Pre-condiciones de `fn-orquestador`** (abortara si no se cumplen):
- Migration `fn_operations/migrations/006_task_runs.sql` aplicada
- Issue con criterios de aceptacion **verificables programaticamente** (no "funciona bien")
- `master` local up-to-date con `origin/master`
- Branch `auto/<issue>` NO existe ya (limpiar previo si hace falta)
- `gh` autenticado (PR draft al converger)
- Tipo soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`
**Aislamiento por worktree**: cada run crea `/tmp/fn_orq_<issue>_<ts>/` via `git worktree add`. Working tree principal del usuario queda intacto. N orquestadores paralelos = N worktrees independientes. `task_runs` persiste en BD del repo principal (auditoria sobrevive aunque borres worktree).
## Otros subagentes utiles
- `Explore` — busquedas amplias en codebase (>3 queries) sin contaminar contexto principal
- `general-purpose` — research multi-step open-ended
## Reglas duras
1. **Paralelo real**: tareas independientes → un mensaje con varios `Agent` calls. NO en serie.
2. **Briefing autocontenido**: subagente no ve historial. Pasar paths absolutos, IDs, criterio exito.
3. **No delegar comprension**: nada de "haz lo que veas". Especificar que cambiar, donde, por que.
4. **Verificar output**: leer diff/resultado, no confiar en resumen del subagente.
5. **No duplicar**: si delegas research, no lo repitas tu.
## Patrones canonicos de paralelismo
- 3 funciones de registry independientes → 3 `fn-constructor` en paralelo
- Auditar N apps → N `fn-recopilador` en paralelo
- Validar varias apps → N `fn-analizador` en paralelo
- Build cpp + tests py + audit operations.db → 3 calls paralelos
- Tras `fn-analizador` con fallos → `fn-mejorador` por cada `run_id`
- Tarea multi-fase autonoma (issue con criterios verificables) → `fn-orquestador` (1 sola run, NO recursivo)
## Anti-patrones
- Escribir funcion reutilizable inline en `apps/` (debe ir a `functions/` via `fn-constructor`)
- Lanzar subagentes en serie cuando son independientes
- Prompt de 1 linea sin contexto ("arregla esto")
- Invocar subagente y luego hacer tu mismo el trabajo
- Spawn `fn-orquestador` sin migration 006 o sin issue verificable (abortara)
- `fn-orquestador` recursivo (un orquestador no spawn-ea otro)
## Checklist pre-respuesta
- ¿>1 tarea independiente? → paralelizar
- ¿Hace falta funcion/tipo nuevo? → `fn-constructor`, NO inline
- ¿Hay que ejecutar/auditar/validar? → fase 2/3/4 segun toque
- ¿`e2e_runs` con fallos? → `fn-mejorador`
- ¿Issue con criterios verificables + tipo soportado? → `fn-orquestador` (chequear pre-condiciones)
- ¿Research amplio (>3 queries)? → `Explore`
## Plantilla minima de prompt para subagente
```
Contexto: <que repo, que app, que objetivo>
Input: <paths absolutos, IDs registry, run_id si aplica>
Tarea: <accion concreta y acotada>
Criterio exito: <como sabe que termino>
Limites: <que NO debe tocar>
Telemetria: tus tool calls quedan registradas en projects/fn_monitoring/apps/call_monitor/operations.db
via hook PostToolUse heredado de settings.local.json. Sigue patrones canonicos
(mcp__registry__fn_*, ./fn run, heredoc importando) — los antipatrones se loguean
como violations.
```
## Telemetria heredada (issue 0085 hardening 5)
Los hooks de `.claude/settings.local.json` se heredan automaticamente por cada sub-agente que Claude Code lance via la tool `Agent`. Eso significa:
- Cada Bash, Edit, Write, MultiEdit, `mcp__registry__*` del sub-agente dispara `hook_call_monitor.sh` exactamente igual que en la sesion principal.
- El `session_id` del JSON de input del hook viene del sub-agente, distinto al de la sesion padre. Util para auditar comportamiento por agente.
- Las violations detectadas (sqlite3 directo, heredoc reinventando, etc) cuentan tambien para sub-agentes — un `fn-constructor` que reescribe inline en lugar de delegar a otro `fn-constructor` queda registrado.
- `FN_TELEMETRY=1` esta en el `env` block de settings.local.json — los heredocs Python/Bash de sub-agentes ya tienen wrappers activos automaticamente.
Implicacion: NO necesitas pasar flags `--telemetry` a sub-agentes. Solo asegurate de que el prompt sigue patrones canonicos. La regla `.claude/rules/registry_calls.md` se aplica igual.
Si un sub-agente abre un proceso hijo que escapa al hook (ej. `nohup ... &`, daemons), ese subproceso queda fuera de la telemetria — documentalo en el prompt si es un caso valido.
+135
View File
@@ -0,0 +1,135 @@
# /validate-app — Validar end-to-end una app del registry
Orquesta la cadena `fn-executor → fn-recopilador → fn-analizador → fn-mejorador` (fases 2-5 del bucle reactivo) sobre una app concreta. Devuelve veredicto pass/fail + IDs de proposals creadas si hay fallos.
## Argumento
`$ARGUMENTS``<app_id>` o `<dir_path>`. Ejemplos:
- `kanban_go_tools`
- `apps/kanban`
- `graph_explorer_cpp_viz`
- `projects/osint_graph/apps/graph_explorer`
Si vacio: detectar app desde `pwd` (si estas dentro de `apps/<X>/` o `projects/*/apps/<X>/`); si no, listar apps con `e2e_checks` declarado y pedir.
## Pasos
### 1. Resolver app objetivo
```bash
ROOT=/home/lucas/fn_registry
ARG="$ARGUMENTS"
if [ -z "$ARG" ]; then
CWD="$(pwd)"
case "$CWD" in
"$ROOT"/apps/*|"$ROOT"/projects/*/apps/*)
ARG="$(realpath --relative-to="$ROOT" "$CWD")"
;;
*)
sqlite3 "$ROOT/registry.db" "SELECT id, dir_path FROM apps ORDER BY id;"
echo "Especifica app_id o dir_path"
exit 1
;;
esac
fi
# Resolver a (id, dir_path)
if echo "$ARG" | grep -q "^apps/\|^projects/"; then
APP_DIR="$ARG"
APP_ID=$(sqlite3 "$ROOT/registry.db" "SELECT id FROM apps WHERE dir_path = '$ARG';")
else
APP_ID="$ARG"
APP_DIR=$(sqlite3 "$ROOT/registry.db" "SELECT dir_path FROM apps WHERE id = '$ARG';")
fi
[ -z "$APP_ID" ] || [ -z "$APP_DIR" ] && { echo "App no encontrada: $ARG"; exit 1; }
```
### 2. Verificar contrato `e2e_checks`
```bash
HAS_CHECKS=$(awk '/^e2e_checks:/,/^[a-z_]+:|^---$/' "$ROOT/$APP_DIR/app.md" | grep -c "^ - id:")
if [ "$HAS_CHECKS" -eq 0 ]; then
echo "App $APP_ID no tiene e2e_checks declarados."
echo "Invocar fn-recopilador design-e2e para generar contrato:"
echo ""
echo " Agent(subagent_type=fn-recopilador, prompt=\"design-e2e $APP_DIR\")"
exit 0
fi
```
### 3. Fase 3 — RECOPILAR (auditar operations.db)
Invocar `fn-recopilador` para confirmar que los datos operativos estan integros antes de validar. Si recopilador reporta FAIL critical, NO continuar.
```
Agent(subagent_type=fn-recopilador,
prompt="Auditar app $APP_DIR. Reportar OK/WARN/FAIL en formato corto.
Si hay FAIL critical, advertirlo claramente. Solo lectura.")
```
Si reporta FAIL critical → abortar con mensaje y no llegar a fn-analizador.
### 4. Fase 4 — ANALIZAR (correr e2e_checks)
```
Agent(subagent_type=fn-analizador,
prompt="Validar end-to-end la app $APP_ID (dir_path: $APP_DIR).
Leer e2e_checks del app.md, ejecutar via e2e_run_checks_go_infra,
evaluar assertions, calcular drift, persistir en e2e_runs.
triggered_by: manual.
git_sha: $(git rev-parse --short HEAD 2>/dev/null || echo '')
Devolver veredicto caveman + run_id.")
```
Capturar `RUN_ID` del output. Capturar `STATUS` (`pass`|`partial`|`fail`).
### 5. Fase 5 — MEJORAR (proposals si hay fallos)
Solo si `STATUS != pass`:
```
Agent(subagent_type=fn-mejorador,
prompt="App $APP_ID tuvo fallos en run_id $RUN_ID.
Leer e2e_runs y summary_json de $APP_DIR/operations.db.
Por cada fail critical: crear proposal kind=new_function|improve_function
en registry.db con created_by=reactive_loop, evidence con run_id+check_id.
Sugerir fix concreto en description.
Devolver lista de proposal_ids creados.")
```
Capturar `PROPOSAL_IDS`.
### 6. Reporte final al usuario
Tabla resumen:
```
=== /validate-app: $APP_ID ===
Fase 3 RECOPILAR: ✓ datos operativos integros
Fase 4 ANALIZAR: <STATUS> (run_id: <RUN_ID>)
<P>/<T> checks pass, <W> warn, <F> fail
Fase 5 MEJORAR: <N> proposals creadas: <PROPOSAL_IDS>
Detalle por check:
build_frontend ✓ 42s
build_backend ✓ 18s
smoke_api ✓ 1.2s
tests_go ✗ 12s — 3/45 fails
Siguientes pasos:
- Revisar proposals: fn proposal list -s pending
- Ver run completo: sqlite3 $APP_DIR/operations.db "SELECT * FROM e2e_runs WHERE id='<RUN_ID>'"
```
## Notas
- **fn-mejorador no existe todavia** (paso 6 del issue 0068). Mientras tanto, si STATUS != pass, solo imprime el detalle del fallo y sugerir crear proposal manual.
- Si un agente subagente devuelve respuesta ambigua (no extrae RUN_ID claramente), pedir clarificacion al usuario antes de continuar.
- Para apps sin `operations.db` (ej. kanban usa `kanban.db`), `e2e_runs` se persiste en `/tmp/<app>_e2e_runs.db` con la misma migracion 005.
- Caveman OK en stdout salvo en mensajes de error donde claridad supera brevedad.
- Tras correr la cadena, NO commitear nada automaticamente. La decision de mergear es del humano.
+22 -1
View File
@@ -11,5 +11,26 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 05 | [stubs.md](stubs.md) | Stubs impuros para dependencias externas |
| 06 | [assertions.md](assertions.md) | Kinds de assertions son texto libre |
| 07 | [proposals.md](proposals.md) | Quien crea proposals y cuando |
| 08 | [tag_launcher.md](tag_launcher.md) | Tag launcher para Pipeline Launcher TUI |
| 08 | [function_tags.md](function_tags.md) | Tags con significado especial: launcher, service |
| 09 | [go_packages.md](go_packages.md) | Nombre de paquete Go = nombre del directorio |
| 10 | [apps_vs_functions.md](apps_vs_functions.md) | Codigo reutilizable en functions/, no reutilizable en apps/ |
| 11 | [sources.md](sources.md) | Extraccion de funciones desde repos externos |
| 12 | [notebook_collaboration.md](notebook_collaboration.md) | Colaboración en notebooks Jupyter via funciones del registry |
| 13 | [frontend_theming.md](frontend_theming.md) | Componentes propios y sistema de temas en frontends |
| 14 | [deploy.md](deploy.md) | Deploy de apps a VPS remotos via SSH + systemd + rsync |
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
| 21 | [playgrounds.md](playgrounds.md) | Prototipos rapidos dentro de un artefacto padre — heredan entorno, no se indexan, no tienen repo propio |
| 22 | [registry_first.md](registry_first.md) | Antes de escribir codigo en un artefacto: buscar en el registry, reutilizar si existe, delegar a `fn-constructor` si falta |
| 23 | [fn_doctor.md](fn_doctor.md) | `fn doctor`: diagnostico read-only de artefactos, services, sync drift, uses_functions, unused — wrappers de funciones del registry |
| 24 | [feature_flags.md](feature_flags.md) | TBD: feature flags para mergear codigo incompleto sin romper master. Patrones por stack (Go/TS/Bash/Py), branch-by-abstraction, anti-patrones |
| 25 | [db_migrations.md](db_migrations.md) | Migraciones SQLite obligatorias para cualquier cambio de schema. Aditivas, idempotentes, archivos numerados. Nunca borrar .db ni modificar migraciones existentes |
| 26 | [e2e_validation.md](e2e_validation.md) | Contrato `e2e_checks` en `app.md` consumido por fn-analizador (fase 4 del bucle reactivo). Issue 0068 |
| 27 | [registry_calls.md](registry_calls.md) | Patrones canonicos para invocar funciones del registry (MCP inspect / MCP run / heredoc compose), antipatrones, excepciones, telemetria. Issue 0085 |
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
+58
View File
@@ -0,0 +1,58 @@
## Trunk-based development (TBD) en apps generadas con `fn`
**El registry NO usa TBD** (push directo a master OK). Pero **toda app generada con `fn`** que viva en `apps/`, `projects/<name>/apps/` o que se despliegue a un VPS via `deploy_server` **DEBE seguir TBD** mientras se desarrolla.
**Tronco unico: `master`** en todos los repos `dataforge/<name>` del ecosistema (apps + analyses). Ver ADR 0002. El default de `git init` debe estar en `master` (`git config --global init.defaultBranch master`) — los pipelines de scaffolding y `ensure_repo_synced_bash_infra` ya pasan `master` explicitamente.
```
master ← siempre deployable
└── issue/<NNNN>-<slug> ← rama efimera (horas)
└── quick/<slug> ← cambios rapidos sin issue
commits atomicos (feat:, fix:, test:, docs:, refactor:, chore:)
merge --no-ff → master → push → delete branch
```
### Reglas
1. **Nunca trabajar directo en master para una app**. Crear `issue/<NNNN>-<slug>` o `quick/<slug>` primero.
2. **Commits atomicos** por bloque logico (no WIP, no mezclar tipos).
3. **Tests obligatorios** antes de mergear (los que aplique al stack: ctest/go test/pytest/...).
4. **`merge --no-ff`** preserva la historia paralela. `git log --first-parent master` da la vista limpia.
5. **Feature flags** (no WIP) cuando una feature no cabe en una sola rama. Archivo: `dev/feature_flags.json`. Detalle: `feature_flags.md`.
### Que hacer cuando aparece WIP en el working tree
Doctrina TBD: **master siempre desplegable**. Si tras implementar un issue queda codigo a medias en otros archivos (modificado pero no terminado), HAY DOS opciones legales:
| Caso | Accion |
|---|---|
| WIP no relacionado al issue, pequeño, ya estable (ej. null-guards de un bug menor) | Incluirlo en el commit del issue **solo si compila + tests pasan**. Mencionarlo en el cuerpo del commit. |
| WIP relacionado al issue pero incompleto | Envolver en feature flag OFF (`enabled: false` en `dev/feature_flags.json`). Mergear codigo terminado y testeado. Activar flag en commit posterior. |
| WIP de otra feature distinta, no terminada | NO mergear con el issue. `git stash` o crear `issue/<otro>-...` para llevarlo aparte. NO romper master. |
| Pre-existing failing tests (no causados por la rama) | Documentar en cuerpo del commit/PR. Crear issue separado para el fix. NO bloquea merge si tu cambio no los introduce. |
**Regla de oro:** ningun commit pusheado a master debe romper el deployment. Si el codigo no esta terminado pero compila + pasa tests, viaja detras de un flag OFF. Si rompe, no sale.
### Por que el registry esta exento
El registry es un repo de funciones reutilizables, no un servicio en produccion. Los cambios son atomicos por su propia naturaleza (una funcion = uno o dos archivos). Imponer TBD a cada `fn add` añadiria fricion sin ganancia: la BD se regenera con `fn index`, no hay deployment, no hay usuarios consumiendo master en directo.
### Cuando aplica TBD
| Cambio | TBD obligatorio |
|---|---|
| Funcion nueva en `cpp/functions/`, `python/functions/`, etc. | NO — push directo a master |
| Tipo nuevo en `types/` | NO |
| Doc/regla en `.claude/`, `docs/` | NO |
| Issue del registry mismo (`dev/issues/`) | NO — issue cerrado y push directo |
| App nueva o modificacion de app en `apps/` o `projects/*/apps/` | **SI** |
| Service desplegable (`tag: service`) | **SI** |
| Analysis en `analysis/` o `projects/*/analysis/` | NO — son exploraciones efimeras |
### Comandos
- `/git-branch` — crea rama desde master actualizado (para apps).
- `/git-push` — tests → merge `--no-ff` → push → eliminar rama (para apps).
Para el registry, push directo a master con commits atomicos.
+18
View File
@@ -0,0 +1,18 @@
Solo codigo reutilizable y componible va en `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/`.
Scripts especificos, dashboards hardcodeados, CLIs de un solo uso, y cualquier codigo que no sea una primitiva componible va en `apps/`. Cada app en `apps/` es independiente: puede importar funciones del registry pero nunca al reves.
Criterios para decidir:
- **functions/**: firma generica, sin credenciales ni config hardcodeada, util en multiples contextos
- **apps/**: orquesta funciones del registry para un caso concreto, tiene config/credenciales, layout fijo
Las apps Python importan funciones del registry con: `sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))` y luego `from <paquete> import ...` (sin prefijo `functions.`).
## temp/ — workspace efimero
`temp/` es un espacio de trabajo desechable para pruebas rapidas: probar una API, un script exploratorio, un analisis puntual, prototipos. Todo gitignored.
- **NO es codigo del registry** — nada en `temp/` se indexa ni se versiona
- **Estructura libre** — subcarpetas por tema: `temp/api_test/`, `temp/quick_analysis/`, etc.
- **Extraccion**: si algo en `temp/` resulta util, se extrae al registry con el flujo normal (como si fuera `sources/`)
- **Limpieza**: se puede borrar el contenido en cualquier momento sin consecuencias
+41
View File
@@ -0,0 +1,41 @@
## Artefactos: termino colectivo
**"Artefacto"** es el termino paraguas para todo lo que vive en el registry pero NO es codigo reutilizable de `functions/` o `types/`. Sirve para no repetir "apps, analysis, vaults, projects, playgrounds" cada vez.
Tipos de artefacto:
| Tipo | Donde vive | Indexado en registry.db | Repo Gitea propio |
|---|---|---|---|
| **app** | `apps/`, `cpp/apps/`, `projects/<p>/apps/<a>/` | tabla `apps` | si (`dataforge/<a>`) |
| **analysis** | `analysis/<t>/`, `projects/<p>/analysis/<t>/` | tabla `analysis` | si (`dataforge/<t>`) |
| **vault** | `projects/<p>/vaults/<v>` (symlink) | tabla `vaults` | no (datos fuera del repo) |
| **project** | `projects/<p>/` | tabla `projects` | no (vive dentro de fn_registry) |
| **playground** | `<artefacto_padre>/playground/` | NO se indexa | no (vive dentro del padre) |
Caracteristicas comunes de los artefactos:
- NO son codigo reutilizable. La reutilizacion vive en `functions/`.
- Tienen ciclo de vida propio (crear, modificar, archivar, borrar).
- `pc_locations` los unifica via `entity_type` (app, analysis, project, vault).
- Pueden importar funciones del registry; el registry NUNCA importa de un artefacto.
### Cuando usar el termino
Usa "artefacto" cuando hablas de varios tipos a la vez o cuando la afirmacion aplica a todos:
- "Cada artefacto declara sus funciones del registry en su `.md`" (vale para apps y analyses).
- "Los artefactos no se importan desde `functions/`."
- "Esta regla aplica a cualquier artefacto desplegable" (apps + services).
Cuando hables de UN tipo concreto, usa el nombre concreto: "esta app...", "este analysis...". No abuses del termino paraguas — es para evitar listas, no para difuminar.
### Que NO es un artefacto
- `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/` — codigo reutilizable.
- `types/`, `python/types/`, `frontend/types/` — tipos del registry.
- `sources/` — repos externos clonados para extraer funciones (gitignored).
- `temp/` — workspace efimero, ni siquiera versionado.
- `subrepos/` — espejos de repos externos para referencia.
### Relacion con `pc_locations`
Los artefactos con presencia en disco (app, analysis, project, vault) ya estan unificados en `pc_locations` via la columna `entity_type`. Los **playgrounds** NO entran en `pc_locations` porque son hijos de otro artefacto y se mueven con el (no tienen identidad propia entre PCs).
+60
View File
@@ -0,0 +1,60 @@
## Capability groups: tags + paginas madre en docs/capabilities/
Un **capability group** es un cluster de >=3 funciones del registry que comparten un dominio operativo (ej. `notebook`, `metabase`, `deploy`). Cada grupo tiene un **tag plano** (sin prefijo) y una **pagina madre** en `docs/capabilities/<grupo>.md`. La pagina madre desbloquea el conjunto entero en un solo read.
### Para que existen
Sin grupos, Claude redescubre funciones via FTS5 una a una cada sesion ("¿como interactuo con Jupyter? ¿como subo deploy?"). Con grupos, Claude lee `docs/capabilities/<grupo>.md` y carga las 5-10 funciones del cluster con su ejemplo canonico — menos turnos perdidos en discovery.
### Convencion de tag
- **Slug del grupo** = tag plano. Ej: `notebook`, `metabase`, `android-emu`.
- **No prefijos** (`cap:`, `group:`). Ya hay namespacing implicito porque convivirian con tags semanticos sueltos.
- **Una funcion puede llevar varios tags de grupo** si pertenece a dos clusters (raro pero valido).
- Filtro MCP: `mcp__registry__fn_search query="" tag="notebook"` lista el grupo.
### Cuando crear grupo nuevo
- **Minimo 3 funciones** afines. Con 2 no compensa pagina madre — quedan tags sueltos.
- **Dominio operativo claro**: el grupo debe ser describible en 1 frase ("operar Jupyter colaborativo", "deploy via SSH+systemd").
- **Frontera neta** con grupos existentes. Si solapa con otro -> reorganizar, no duplicar.
### Como crear grupo
1. Anadir el tag al frontmatter `.md` de >=3 funciones afines. `fn index` lo registra.
2. Crear `docs/capabilities/<grupo>.md` con plantilla:
- **Lista de funciones**: tabla `ID | firma corta | que hace`.
- **Ejemplo canonico**: 1-2 bloques de codigo end-to-end con los IDs reales.
- **Fronteras**: que NO cubre el grupo.
- **Prerequisitos** y **notas** si aplica.
3. Anadir fila al `docs/capabilities/INDEX.md`.
4. Correr `fn doctor capabilities` para auditar drift.
### Auto-generacion
`fn doctor capabilities --update` (TBD) reescribe la tabla de funciones de cada pagina madre preservando bloques curated (`Ejemplo canonico`, `Fronteras`, `Notas`). Las secciones curated nunca se sobrescriben.
### Como Claude usa los grupos
Cuando una tarea cae en un dominio conocido:
1. `Read docs/capabilities/INDEX.md` para localizar grupo.
2. `Read docs/capabilities/<grupo>.md` para cargar funciones + ejemplo.
3. Solo si el grupo no cubre lo necesario, `mcp__registry__fn_search` para funciones sueltas.
4. Si el grupo deberia cubrir pero falta funcion -> `fn-constructor` + tagear con el grupo en el frontmatter.
### Auditoria
```bash
fn doctor capabilities # lista grupos + drift
fn doctor capabilities --json # para agentes
```
Comprueba:
- Tag con N >=3 funciones pero sin pagina madre -> "tag huerfano".
- Pagina madre sin tag respaldo -> "grupo fantasma".
- Funcion con tag de grupo pero la pagina madre no la lista (autogen desfasada) -> "drift".
### Relacion con dominios
Los **dominios** del registry (`core`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `tui`, `pipelines`, `browser`) son taxonomia ortogonal — un grupo puede atravesar varios dominios (ej. `deploy` toca `infra` y `shell`). NO renombrar dominio a grupo ni viceversa.
+263
View File
@@ -0,0 +1,263 @@
## Estandarizacion de apps C++ del registry
**Fuentes autoritativas:**
- `cpp/PATTERNS.md` — checklist y esqueleto del app shell (`fn::run_app`, AppConfig, panels, layouts, Settings, About).
- `cpp/DESIGN_SYSTEM.md` — identidad visual (`fn_tokens`, ThemeMode, equivalencias `@fn_library` ↔ C++).
Esta regla NO duplica esos documentos — los señala como obligatorios y añade convenciones estructurales que no aparecen alli.
### Scaffolder canonico — OBLIGATORIO
**REGLA DURA:** crear apps C++ nuevas SIEMPRE con `fn run init_cpp_app <name> [--project <p>] [--desc "..."]`. NUNCA escribir `main.cpp` + `CMakeLists.txt` + `app.md` desde cero a mano en `cpp/apps/` ni `projects/*/apps/`. Tampoco copiar otra app y renombrar — la deriva entre patrones es lo que estamos eliminando.
Si el scaffolder no cubre un caso (ej. necesitas plantilla diferente, layout custom desde el primer dia), **modificas el scaffolder**, no escribes la app a mano. La plantilla canonica es codigo, no decoracion.
Razones:
- Garantiza `cfg.about` + `cfg.log` + `cfg.panels` + framework defaults aplicados.
- Genera frontmatter `app.md` valido (framework, dir_path, repo_url) para `fn index`.
- Registra `add_subdirectory` en `cpp/CMakeLists.txt` (raiz o bloque `_DIR` para projects).
- Crea repo Gitea `dataforge/<name>` con master + commit inicial.
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
### 1. Ubicacion
| Caso | Donde vive |
|---|---|
| App independiente | `cpp/apps/<nombre>/` |
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
### 2. Estructura minima
```
<app_dir>/
CMakeLists.txt # usa add_imgui_app(target ...)
app.md # frontmatter de registro (ver §4)
main.cpp # entry: parseo de args + fn::run_app + render()
[data.{h,cpp}] # opcional: capa de datos (DB / HTTP / archivos)
[views.{h,cpp}] # opcional: composicion de paneles
[<modulo>.{h,cpp}] # opcional: dominio especifico
[vendor/] # opcional: deps no comunes (se prefieren las globales en cpp/vendor/)
[.git/] # cada app es su propio repo Gitea (ver §6)
```
**Reglas de split:**
- `main.cpp` SIEMPRE — punto de entrada con `int main()` + `fn::run_app(...)` + funcion `render()`.
- Si la app supera ~400 lineas en `main.cpp`, partir en `data.{h,cpp}` (carga/persistencia) + `views.{h,cpp}` (UI por panel).
- Modulos especificos del dominio en archivos propios (`compiler.cpp` en `shaders_lab`, `data_http.cpp` en `registry_dashboard`).
- NO crear archivos de "utilidades genericas" dentro de la app — eso va al registry como funcion (`cpp/functions/...`).
### 3. CMakeLists.txt
Patron canonico:
```cmake
add_imgui_app(<target>
main.cpp
[extra_modules.cpp]
# Funciones del registry usadas (paths absolutos):
${CMAKE_SOURCE_DIR}/functions/<dominio>/<funcion>.cpp
...
)
target_include_directories(<target> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(<target> PRIVATE [SQLite::SQLite3] [imgui_node_editor] ...)
if(WIN32)
set_target_properties(<target> PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
```
Reglas:
- Usar SIEMPRE la macro `add_imgui_app(target ...)` — gestiona enlace con `fn_framework` y copia de TTFs.
- Listar explicitamente cada `.cpp` del registry usado (no glob). Hace visible el grafo de dependencias.
- NO listar `tokens.cpp`, `icon_font.cpp`, `app_settings.cpp`, `app_about.cpp`, `fps_overlay.cpp`, `panel_menu.cpp`, `app_menubar.cpp`, `layouts_menu.cpp`, `gl_loader.cpp`, `layout_storage.cpp` — viven en `fn_framework` y dan multiple-definition si se duplican.
- En `WIN32`, marcar `WIN32_EXECUTABLE TRUE` para apps GUI (sin consola).
### 4. app.md (frontmatter)
Plantilla minima para apps C++:
```yaml
---
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
description: "Frase corta — lo que hace y por que existe."
tags: [imgui, ...] # si es service, anadir 'service'
uses_functions: # IDs del registry — el indexer NO deduce C++
- <nombre>_cpp_<dominio>
- ...
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/<name>" o "projects/<proyecto>/apps/<name>"
repo_url: "https://gitea-.../dataforge/<name>"
---
```
Reglas:
- `uses_functions` se rellena a mano con los IDs de las funciones del registry usadas en `CMakeLists.txt`. Auditar con: `sqlite3 registry.db "SELECT id FROM apps WHERE id='<id>';"` + revisar diffs.
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
- `repo_url` apunta al sub-repo en Gitea (ver §6).
### 5. Registro en `cpp/CMakeLists.txt`
Cada app nueva se registra al final de `cpp/CMakeLists.txt`:
```cmake
# --- <app_name> ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/<name>/CMakeLists.txt)
add_subdirectory(apps/<name>)
endif()
```
Para apps en proyectos (fuera del arbol `cpp/`):
```cmake
# --- <app_name> (lives in projects/<proj>/apps/) ---
set(_<NAME>_DIR ${CMAKE_SOURCE_DIR}/../projects/<proj>/apps/<name>)
if(EXISTS ${_<NAME>_DIR}/CMakeLists.txt)
add_subdirectory(${_<NAME>_DIR} ${CMAKE_BINARY_DIR}/apps/<name>)
endif()
```
El `if(EXISTS ...)` hace el registro tolerante a apps no clonadas (cada app es sub-repo separado).
### 6. Sub-repo Gitea (TBD obligatorio)
Cada app C++ es su propio repo en `dataforge/<name>` con branch `master`. Esto significa:
- El directorio `<app_dir>/` esta en el `.gitignore` de `fn_registry` (excepto `app.md`).
- El propio directorio tiene `.git/` apuntando al sub-repo.
- TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/<NNNN>-<slug>` o `quick/<slug>`, mergear a `master` con `--no-ff`.
- Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`.
### 7. Convencion `local_files/` — separacion de distribuible vs estado local
**OBLIGATORIO**: TODA app coloca sus archivos escribibles bajo
`<exe_dir>/local_files/`. Los archivos distribuibles (`.exe`, `.dll`,
`.ttf`, `enrichers/`, `runtime/`) viven directos en `<exe_dir>/`.
```
<exe_dir>/
├── <app>.exe
├── duckdb.dll, *.ttf, runtime/, enrichers/ ← read-only, ships con el zip
└── local_files/ ← writable, per-PC
├── imgui.ini ← gestionado por fn::run_app
├── app_settings.ini ← gestionado por fn_ui::settings_*
└── <lo que la app escriba> ← usar fn::local_path("nombre")
```
`fn::run_app` lo gestiona automaticamente para `imgui.ini` y
`app_settings.ini` y migra desde `<exe_dir>/` o `cwd` si vienen de
una version previa.
Apps que escriban archivos extra (DBs, caches, proyectos del
usuario) **DEBEN** usar `fn::local_path("nombre")` al construir
sus paths. Ejemplo:
```cpp
// MAL
sqlite3_open("graph_explorer.db", &db);
fopen("graph_explorer.ini", "r");
// BIEN
sqlite3_open(fn::local_path("graph_explorer.db"), &db);
fopen(fn::local_path("graph_explorer.ini"), "r");
```
API en `cpp/framework/app_base.h`:
- `fn::exe_dir()` — directorio del ejecutable.
- `fn::local_dir()``<exe_dir>/local_files/`, creado on-demand.
- `fn::local_path(name)``<local_dir>/<name>`.
- `fn::migrate_to_local_files(names, n)` — mueve archivos viejos.
Beneficios:
- Carpeta del .exe limpia para distribuir (zip portable).
- Reset trivial (basta borrar `local_files/`).
- Separacion clara para backup/sync (solo `local_files/` es propio del PC).
### 7.1 Anti-jitter automatico (AltSnap, tiling WMs)
`fn::run_app` aplica tres capas de proteccion contra jitter al mover la
ventana con herramientas externas (AltSnap en Windows, snap-assist, tiling
WMs). Activado por defecto, sin opt-in:
1. **GLFW pos/size callbacks**`vp->Pos/Size` se sincronizan al instante
con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame).
2. **Per-frame viewport sync** al inicio del main loop — cubre viewports
secundarios (paneles drag-out) que la backend crea dinamicamente.
3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE`
/ `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras
el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`,
replicando el contrato del title-bar drag native (DefWindowProc bloquea
el hilo, DWM compositor mueve el framebuffer existente).
Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases:
- `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta
`vp->Pos` sigue OS dentro de 1px.
- `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` +
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta
que `render()` no se llama durante el bracket.
Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`.
NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app
necesita renderizar incluso durante external move (caso raro: telemetria
en vivo, video stream), tendria que evitar el bypass — actualmente no hay
flag para desactivarlo (anadir `cfg.pause_on_external_sizemove = true` por
default si surge necesidad).
### 8. Convenciones de runtime
Cumplir el checklist completo de `cpp/PATTERNS.md`. Resumen de lo que NUNCA debe aparecer en una app:
| Anti-patron | Sustituir por |
|---|---|
| `glfwInit()` en `main` | `fn::run_app(cfg, render)` |
| `ImGui::StyleColorsDark()` | `cfg.theme = ThemeMode::FnDark` (default) |
| `ImVec4(0.5,0.5,0.5,1)` | `fn_tokens::colors::*` |
| `ImGui::Begin(u8"\xEF...")` | `ImGui::Begin(TI_HOME " ...")` |
| Menubar inline cada frame | `cfg.panels` + `cfg.layouts_cb` |
| About hardcoded en un panel | `cfg.about = {...}` |
| `gl*` directo sin loader | `cfg.init_gl_loader = true` |
| Tabla SQLite en la raiz del repo | `<app_dir>/<app>.db` (operations.db es solo para entities/relations/executions) |
| `fopen("foo.ini", ...)` con path relativo | `fopen(fn::local_path("foo.ini"), ...)` (ver §7) |
### 8. Tests visuales (recomendado, no obligatorio)
Si la app tiene componentes que se quieren proteger contra regresiones visuales, anadir un demo en `cpp/apps/primitives_gallery/demos_<dominio>.cpp` que use los mismos componentes/funciones del registry. El sistema de capture-and-compare de `primitives_gallery --capture` funciona como golden-image gate (ver final de `cpp/PATTERNS.md`).
### 9. Decisiones que cada app debe tomar y documentar en su `app.md`
- `viewports`: `true` (default) si las ventanas pueden arrastrarse fuera del main; `false` si la app necesita estar siempre embebida.
- `init_gl_loader`: `true` si llama `gl*` directo (renderers GPU custom como `graph_renderer`); `false` si solo usa ImGui/ImPlot.
- `about` info: nombre, version (semver), descripcion 1 frase.
- Persistencia: `<app>.db` SQLite junto al exe; nunca tocar `registry.db` ni `operations.db` salvo lectura.
- Modo CLI: si la app acepta args, documentarlos en el `app.md` con ejemplos.
### 10. Layouts persistentes (default)
`fn::run_app` provee menu Layouts (Save current as.../Apply/Delete/Reset) sin
codigo. Crea `<exe_dir>/local_files/layouts.db` (tabla `imgui_layouts` +
`layout_meta`) y persiste el `imgui.ini` serializado por nombre.
**Restore-on-open / save-on-close (1.1.0+):** al cerrar la app, el slot del
layout activo se reescribe con el `imgui.ini` actual (los retoques de
docking sobreviven). Al abrir, si habia un layout activo persistido en
`layout_meta.last_active`, se carga en el primer frame. Si la app no usa
named layouts (nunca clico Save/Apply), el comportamiento sigue siendo el
de antes: `imgui.ini` es la unica fuente.
- App nueva: nada que tocar — Layouts viene activo.
- App quiere personalizar `on_reset` (ej. re-mostrar paneles especificos como
`shaders_lab`): abre su propio `LayoutStorage`, llama
`layout_storage_make_callbacks`, override `on_reset`, y pasa
`cfg.layouts_cb = &cb`. Cuando se pasa `layouts_cb`, el auto-storage se
desactiva y la app es responsable de `layout_storage_apply_pending` al
inicio de su `render`.
- App headless / capture mode: `cfg.auto_layouts = false`.
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
`local_files/`).
+165
View File
@@ -0,0 +1,165 @@
## Migraciones de BBDD: nunca perder datos
**Regla absoluta:** todo cambio de schema en SQLite (apps con `kanban.db`, `operations.db` propia, registry.db, etc.) DEBE ir en un archivo de migración versionado. Nunca borrar/recrear tablas, nunca cambiar tipos sin proceso seguro, nunca confiar en "borra el .db y vuelve a empezar".
### Por que
- Las apps almacenan **datos vivos** (cards, entities, executions, assertions, columns, sessions).
- Borrar = perder horas/dias/semanas de trabajo del usuario.
- Lo que es trivial en dev (`rm operations.db`) es destructivo en produccion (deploys + sync entre PCs).
- Sync entre PCs (`fn sync`, `/full-git-pull`) trae bases de datos de otros equipos: si tu schema asume tabla recreada, los datos del otro PC desaparecen.
### Patrones obligatorios
#### 1. Archivos numerados en `migrations/`
Cada cambio de schema = un archivo nuevo `migrations/NNN_<accion>.sql`. Numeracion zero-padded de 3 digitos. Nombre descriptivo.
```
apps/<app>/migrations/
001_init.sql # CREATE TABLE inicial (no se modifica nunca)
002_add_stickers.sql # ALTER TABLE cards ADD COLUMN stickers
003_add_assignees.sql # ALTER TABLE cards ADD COLUMN assignee_id
004_create_lock_history.sql # CREATE TABLE card_lock_history
...
```
#### 2. Solo operaciones aditivas seguras
| Operacion | Seguro | Notas |
|---|---|---|
| `CREATE TABLE IF NOT EXISTS` | si | idempotente |
| `CREATE INDEX IF NOT EXISTS` | si | idempotente |
| `ALTER TABLE ... ADD COLUMN` | si | aditivo, default obligatorio |
| `INSERT INTO ... ON CONFLICT IGNORE` | si | seed data idempotente |
| `DROP TABLE` | NO | destructivo |
| `DROP COLUMN` | NO | destructivo (SQLite < 3.35 ni siquiera lo soporta) |
| `ALTER TABLE ... RENAME COLUMN` | precaucion | rompe codigo viejo si rollback |
| `ALTER TABLE ... DROP/ALTER constraint` | NO sin backup | requiere recreate-and-copy |
Si necesitas cambiar tipo, eliminar columna, o cambiar PK: hacer **migracion en pasos** (Branch by Abstraction):
1. Crear nueva columna/tabla con la forma deseada (migration N).
2. App escribe en ambas (migration N+1, codigo).
3. Backfill de datos viejos (migration N+2, script).
4. App lee solo de la nueva (migration N+3, codigo).
5. Eliminar la vieja (migration N+4, despues de tener backups verificados).
Cada paso = una rama TBD corta + commit + verificacion. Nunca un solo PR que rompa lectores.
#### 3. Aplicacion idempotente al arrancar
La app aplica todas las migraciones en orden al iniciar. Patron canonico (Go):
```go
//go:embed migrations/*.sql
var migrationsFS embed.FS
func applyMigrations(conn *sql.DB) error {
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
if err != nil { return err }
sort.Strings(files)
for _, f := range files {
b, err := migrationsFS.ReadFile(f)
if err != nil { return err }
if _, err := conn.Exec(string(b)); err != nil {
// SQLite ALTER TABLE ADD COLUMN no es idempotente nativamente.
// Si ya existe, ignorar el error de "duplicate column".
if !strings.Contains(err.Error(), "duplicate column") &&
!strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("%s: %w", f, err)
}
}
}
return nil
}
```
Alternativa: tabla `_migrations` con las versiones aplicadas (mas robusta para schemas grandes). Para apps pequeñas (kanban, operations.db), bastan los archivos numerados + `IF NOT EXISTS` / catch de "duplicate column".
#### 4. Migracion + cambios en codigo en el mismo commit
Cuando añades una columna:
- `migrations/NNN_<accion>.sql` (nueva)
- `db.go` (lee/escribe la columna)
- `types.ts` (frontend type)
- Tests
Todo en el mismo commit/rama. Si solo mergeas la migracion pero no el codigo, otros PCs aplican la migracion al sync y luego el codigo viejo no la usa. OK. Si mergeas el codigo sin la migracion, la app peta al arrancar en otros PCs. Mal. **Migracion antes que codigo en el orden de archivos** (no de tiempo).
#### 5. Tests sobre la migracion
Cada migracion debe tener test que:
- Arranca con DB vacia → aplica todas → verifica schema.
- Arranca con DB en estado N-1 (datos previos) → aplica migracion N → verifica que los datos se conservan.
Esto detecta migraciones destructivas antes de mergear.
### Que NO hacer
| Anti-patron | Consecuencia |
|---|---|
| Borrar `*.db` durante dev y commitear "schema actualizado" | Otros PCs pierden datos al sync. |
| Modificar `001_init.sql` para añadir columnas | Las DBs ya creadas no se actualizan. Datos divergentes. |
| `DROP TABLE x; CREATE TABLE x ...` | Borra todo lo que el usuario tenga. |
| Usar `ensureColumns` sin archivo SQL paralelo | El cambio de schema vive solo en codigo Go, no auditable, no migrable manualmente. |
| Cambiar tipo de columna in-place | SQLite necesita recreate-and-copy. Asume que pierde datos si no se hace bien. |
| "fn index" como solucion para regenerar registry.db | OK para `registry.db` (regenerable). NUNCA para `operations.db`, `kanban.db`, etc. |
### Casos especiales
#### registry.db (raiz del fn_registry)
`registry.db` SE PUEDE regenerar con `fn index` desde los `.go` y `.md`. Para cambios de schema del registry: actualizar `registry/migrations.go` o el codigo de creacion + `fn index`. NO hace falta archivo de migracion porque la fuente de verdad son los `.md`/`.go`. Excepcion: tablas con datos vivos (`proposals`, `pc_locations`) — esas SI requieren migracion preservando datos.
#### operations.db (por app)
Cada app tiene su `operations.db` con entities/relations/executions. Schema definido en `fn_operations/`. Cambios al schema → archivo de migracion en `fn_operations/migrations/` aplicado al abrir la BD. Idempotente.
#### apps con BD propia (kanban, etc.)
Mismo patron: `apps/<app>/migrations/NNN_*.sql`, embebido y aplicado al arrancar.
### Comandos utiles
```bash
# Ver schema actual
sqlite3 apps/kanban/operations.db ".schema"
# Ver columnas de una tabla
sqlite3 apps/kanban/operations.db "PRAGMA table_info(cards);"
# Backup antes de migracion arriesgada
sqlite3 apps/kanban/operations.db ".backup apps/kanban/operations.db.bak.$(date +%Y%m%d)"
# Aplicar una migracion manual (si la app no esta corriendo)
sqlite3 apps/kanban/operations.db < apps/kanban/migrations/00X_<accion>.sql
# Listar archivos de migracion en orden
ls apps/kanban/migrations/*.sql | sort
```
### Resumen
- Cada cambio de schema = archivo numerado nuevo en `migrations/`.
- Aditivo siempre que se pueda. Destructivo solo en pasos verificados con backup.
- App aplica migraciones al arrancar, idempotente.
- Migracion + codigo + tests en el mismo commit.
- Nunca borrar `.db` para "arreglar" schema. Nunca modificar migraciones existentes.
### Estado retroactivo (2026-05-09)
Inventario de BDs del ecosistema y conformidad con la regla:
| Repo / App | BD | `migrations/` | Estado |
|---|---|---|---|
| `registry/` | `registry.db` | si (11 archivos) | ✓ |
| `fn_operations/` | `operations.db` por app | si (4 archivos) | ✓ |
| `apps/kanban/` | `operations.db` (kanban) | si (5 archivos: 001 init, 002 stickers, 003 columns_extras, 004 cards_extras, 005 history_actor) | ✓ |
| `apps/deploy_server/` | `operations.db` (deploys) | si (2 archivos: 001 init, 002 target_extras) | ✓ |
| `apps/dag_engine/store/` | DB del dag_engine | si (001_init) | ✓ |
| `projects/element_agents/.../shell/memory/` | memoria del agente | si (001_init) | ✓ |
| `projects/osint_graph/apps/graph_explorer/` | DBs C++ inline (project_manager, layout_store, jobs, node_groups) | NO | **pendiente** — refactor C++ multi-archivo, mover schema inline a `migrations/*.sql` aplicado al abrir cada DB. |
Las apps marcadas ✓ usan el patron canonico `embed.FS + applyMigrations()` (Go) o equivalente. La C++ pendiente requiere ronda dedicada — tracker via issue cuando se aborde.
`apps/kanban/db.go::ensureColumns` se mantiene como **backstop idempotente** para DBs muy antiguas creadas antes del refactor de migraciones. NO añadir columnas nuevas alli — siempre via archivo SQL.
+42
View File
@@ -0,0 +1,42 @@
## Delegacion: spawn fn-constructor en vez de escribir inline
**REGLA DURA.** Si vas a escribir logica reutilizable inline en un artefacto (app, analysis, playground) o heredoc, STOP y delega a `fn-constructor`. La misma sesion debe crear + usar la funcion. No acumular huerfanas.
### Cuando un patron es candidato a funcion
- Aparece >=2 veces en esta sesion o en heredocs recientes.
- Firma generica (no depende de tipos internos del artefacto).
- 1 responsabilidad clara (CRUD, parse, transform, http call, formato fijo, etc.).
- No es one-liner idiomatico de stdlib (`time.Now().UTC().Format(...)` queda fuera).
### Flujo obligatorio (mismo turno)
1. **Detectar**. Si vas a escribir >=5 lineas de logica reutilizable inline -> STOP.
2. **Spawn `fn-constructor` inmediato** via `Agent(subagent_type="fn-constructor", ...)`:
- **Sin preguntar al usuario** (autorizado por defecto).
- Si hay >1 funcion independiente -> una sola llamada al Agent tool con **N tool_use blocks paralelos** en el mismo mensaje. NO serializar.
3. **Tagear con grupo de capacidad** al menos UN tag de grupo (`notebook`, `metabase`, `deploy`, etc.). Ver `capability_groups.md`.
4. **`fn index`** para registrar.
5. **Importar + invocar en el mismo turno** — no dejar funcion huerfana recien creada.
6. **Auto-verificar** con `fn doctor uses-functions` y `fn doctor unused` si tocas >=3 funciones nuevas.
### Anti-patrones auditables
| Anti-patron | Consecuencia | Sustituir por |
|---|---|---|
| Escribir helper inline en artefacto en vez de delegar | Reinvento por sesion | Spawn fn-constructor |
| Crear N funciones serialmente | Latencia x N | Multiples `Agent()` en mismo mensaje |
| Crear funcion y no usarla en el turno | Huerfana desde dia 1 (`calls_90d=0`) | Importar + invocar antes de cerrar turno |
| Crear funcion sin tag de grupo | Imposible descubrir en bloque proxima sesion | Anadir tag de grupo (capability group) |
| Reescribir en heredoc logica que ya existe | Capitalizacion perdida | `mcp__registry__fn_search` antes de escribir |
### Excepciones
- **Logica de dominio especifica del artefacto** (CRUD de tabla concreta, layout de UI, flujo unico de la app) -> queda en el artefacto. Solo lo reutilizable se delega.
- **Stub temporal con `not implemented`**: aceptable si la dependencia externa no esta disponible. Documentar en `.md` (ver `stubs.md`).
### Telemetria
Cada `code_writes` + `calls` se registra en `call_monitor/operations.db` (issue 0085). Vista `session_capability_growth` mide ratio creadas vs usadas por sesion. Hook `UserPromptSubmit` inyecta `CAPABILITY-GROWTH: created_this_session=X used=Y orphan=Z` en cada turno.
Si `orphan>0` al cerrar la sesion -> revisar: o la funcion era especulativa (no debio crearse) o falta integrarla en el codigo del artefacto.
+134
View File
@@ -0,0 +1,134 @@
## Deploy de apps a VPS remotos
### Arquitectura
El sistema de deploy usa SSH + systemd + rsync. No Docker, no Kubernetes.
- **Conexiones SSH** → `~/.ssh/config` (alias, IP, user, key). Ya hay funciones CRUD: `ssh_config_read`, `ssh_config_find`, `ssh_config_parse`.
- **Config de deploy** → `apps/deploy_server/operations.db` tabla `deploy_targets` (app, host, remote_dir, build_cmd, port, health_path, env).
- **Logs de deploy** → misma BD, tabla `deploy_logs` (app, host, status, trigger, duration_ms, error).
### App: `deploy_server` (`apps/deploy_server/`)
CLI + servidor HTTP. Binario: `deploy_server`. Build: `CGO_ENABLED=1 go build -o deploy_server .`
```bash
cd apps/deploy_server
# Gestionar targets
./deploy_server target add --app <app> --host <ssh_alias> --port <N> --health /path --build "comando" [--user deploy] [--env '{"K":"V"}']
./deploy_server target list
./deploy_server target remove <app>
# Setup inicial (primera vez, crea dirs + systemd unit)
./deploy_server setup <app> --host <ssh_alias>
# Deploy continuo (build local → rsync → restart → health check)
./deploy_server deploy <app> [--host <ssh_alias>]
# Estado del servicio remoto
./deploy_server status <app>
./deploy_server status --all
# Servidor webhook (auto-deploy en cada push a Gitea)
./deploy_server serve --port 9090
```
### Funciones del registry involucradas
| Función | Qué hace | Purity |
|---|---|---|
| `rsync_deploy_bash_infra` | rsync local→remoto con exclusiones | impure |
| `systemd_generate_unit_go_infra` | Genera texto .service | **pure** |
| `systemd_install_go_infra` | Sube unit + daemon-reload + enable + start | impure |
| `systemd_restart_go_infra` | Reinicia servicio remoto | impure |
| `systemd_status_go_infra` | Estado + logs de servicio remoto | impure |
| `vps_setup_app_go_infra` | Crea dirs + usuario en VPS | impure |
| `gitea_create_webhook_bash_infra` | Crea webhook push en Gitea | impure |
| `setup_vps_app_go_infra` | Pipeline: setup completo primera vez | impure |
| `deploy_app_remote_go_infra` | Pipeline: deploy continuo | impure |
Tipo: `DeployConfig_go_infra` — struct con toda la config de deploy.
### Workflow para un agente
Cuando el usuario diga **"sube esta app a este VPS"** o **"deploya X en Y"**:
#### 1. Verificar que el host SSH existe
```bash
grep "^Host " ~/.ssh/config
# Si no existe el alias, añadirlo:
# Usar ssh_config_add_entry o editar ~/.ssh/config directamente
```
#### 2. Verificar conectividad
```bash
ssh -o BatchMode=yes -o ConnectTimeout=5 <alias> true
```
#### 3. Registrar el target en deploy_server
```bash
cd apps/deploy_server
# Build deploy_server si no existe el binario
CGO_ENABLED=1 go build -o deploy_server .
./deploy_server target add \
--app <nombre_app> \
--host <ssh_alias> \
--port <puerto> \
--health <path_o_vacio> \
--build "CGO_ENABLED=0 GOOS=linux go build -o <binario> ." \
--user deploy
```
#### 4. Setup inicial
```bash
./deploy_server setup <app> --host <ssh_alias>
```
Esto crea dirs en `/opt/apps/<app>/`, sube el código, genera el unit systemd e instala el servicio.
#### 5. Deploys posteriores
```bash
./deploy_server deploy <app>
```
Build local → rsync → restart systemd → health check.
#### 6. Auto-deploy con webhook (opcional)
```bash
# Lanzar servidor
./deploy_server serve --port 9090
# Crear webhook en Gitea
source bash/functions/infra/gitea_create_webhook.sh
gitea_create_webhook "<owner>" "<repo>" "http://<ip_deploy_server>:9090/webhook/push" "<secret>"
```
### Requisitos en el VPS
- SSH accesible con key auth (configurado en `~/.ssh/config` local)
- El usuario SSH debe tener **sudo sin password** para: `systemctl`, `mv` a `/etc/systemd/system/`, `mkdir` en `/opt/apps/`, `useradd`, `chown`
- `rsync` instalado en el VPS
- Puerto del servicio abierto en el firewall del VPS
### Builds por lenguaje
| Lenguaje | Build command típico |
|---|---|
| Go | `CGO_ENABLED=0 GOOS=linux go build -o <nombre> .` |
| Go + SQLite | `CGO_ENABLED=1 GOOS=linux go build -tags fts5 -o <nombre> .` |
| Python | No build — rsync sube los .py, systemd ejecuta `python3 main.py` |
| Bash | No build — rsync sube los .sh, systemd ejecuta `bash main.sh` |
Para Go con CGO (SQLite), el VPS debe tener `gcc` y `libc-dev`, o cross-compilar con `CGO_ENABLED=0` si la app no usa SQLite.
### Exclusiones de rsync
El deploy excluye automáticamente: `.git`, `operations.db*`, `*.exe`, `node_modules`, `.venv`, `__pycache__`, `build/`, `*.db-shm`, `*.db-wal`, `registry.db`.
+162
View File
@@ -0,0 +1,162 @@
## Validacion end-to-end de apps (bucle reactivo, fase 4)
**Contrato obligatorio para apps que vayan a master con gate automatico**: declarar `e2e_checks` en su `app.md`. Sin contrato, `fn-analizador` no puede validar y la app cae al modo "manual": el humano sigue iterando.
Ver tambien: `apps_tbd.md`, `feature_flags.md`, issue 0068.
### Por que
El bucle reactivo del registry tiene 5 fases. Las 3 primeras (`fn-constructor`, `fn-executor`, `fn-recopilador`) cubren CONSTRUIR/EJECUTAR/RECOPILAR. La fase 4 (ANALIZAR) y la 5 (MEJORAR) no funcionan sin un contrato explicito de "como sabe el agente que esta app esta sana". Ese contrato es `e2e_checks`.
### Donde vive
En el frontmatter de cada `app.md`, lista `e2e_checks`. Convencion: `id` unico por check, ejecucion en orden declarado, falla = stop o continue segun severidad (TBD por implementar).
### Tipos de check
| Campo | Que hace |
|---|---|
| `id` | Identificador unico del check dentro de la app (`build`, `smoke`, `tests_unit`, ...) |
| `cmd` | Comando shell. Exit 0 = pass salvo override de `expect_exit`. |
| `health` | URL HTTP. Hace GET, espera 200, util tras un `cmd` que arranca un servicio en background (con `&`). |
| `ref` | Referencia a otro agente / funcion del registry (ej. `fn-recopilador:apps/X`, `fn-doctor:artefacts`). |
| `timeout_s` | Timeout en segundos. Default 60. |
| `expect_exit` | Codigo de salida esperado (default 0). |
| `expect_stdout_contains` | Substring que debe aparecer en stdout. |
| `expect_stdout_json` | JSONPath o key=value que debe satisfacer la salida. |
| `severity` | `critical` (default) o `warning`. Critical = bloquea merge; warning = registra y sigue. |
### Patrones por stack
#### Go service con frontend embebido
```yaml
e2e_checks:
- id: build_frontend
cmd: "cd frontend && pnpm install --frozen-lockfile && pnpm build"
timeout_s: 180
- id: build_backend
cmd: "CGO_ENABLED=1 go build -tags fts5 -o myapp ."
- id: smoke
cmd: "./myapp --port 8200 --db /tmp/myapp_e2e.db &"
health: "http://127.0.0.1:8200/api/health"
- id: tests
cmd: "go test -tags fts5 -count=1 ./..."
```
#### C++ ImGui app
```yaml
e2e_checks:
- id: build
cmd: "cmake --build build --target myapp -j"
timeout_s: 300
- id: self_test
cmd: "./build/myapp --self-test"
timeout_s: 30
- id: pytest
cmd: "cd tests && python3 -m pytest -x -q"
```
Apps C++ deben implementar `--self-test` que arranca, verifica subsistemas (GL loader, fonts, DBs locales), y sale con codigo 0/1.
#### Python pipeline / CLI
```yaml
e2e_checks:
- id: import
cmd: "python3 -c 'import myapp'"
- id: cli_help
cmd: "python3 -m myapp --help"
expect_stdout_contains: "usage:"
- id: dry_run
cmd: "python3 -m myapp --dry-run --input examples/sample.json"
```
#### App con operations.db
Anadir siempre:
```yaml
- id: ops_audit
ref: "fn-recopilador:apps/myapp"
```
Esto invoca al recopilador en modo audit sobre `apps/myapp/operations.db`.
### Reglas
1. **Idempotente**: cada check debe poderse correr N veces sin efectos secundarios. Usar BDs en `/tmp/`, puertos altos, `--port 0` cuando se pueda.
2. **Sin credenciales reales**: ningun check toca produccion ni servicios externos sensibles. Si necesita HTTP de prueba, usar `httpbin.org` o un mock local.
3. **Tiempo acotado**: cada check declara `timeout_s`. Suma total de la app < 10 min como objetivo razonable.
4. **Determinista**: si el check depende de red flaky, marcalo `severity: warning` o usalo solo como diagnostico, no como gate.
5. **Cleanup implicito**: si el check arranca un proceso en background (`&`), debe morir al final. `fn-analizador` mata el grupo de procesos al terminar la suite.
### Como diseñar `e2e_checks` para una app existente
`fn-recopilador` tiene un modo `design-e2e <app_id>` que:
1. Inspecciona `app.md` (lang, framework, entry_point, uses_functions).
2. Revisa estructura del directorio (presencia de `tests/`, `frontend/`, `Makefile`, `CMakeLists.txt`, etc.).
3. Audita `operations.db` (si existe) para sugerir `ops_audit`.
4. Devuelve bloque `e2e_checks_suggested:` listo para copiar al `app.md` tras revision humana.
Comando indicativo:
```
Agent(subagent_type="fn-recopilador",
prompt="design-e2e apps/<app>")
```
El recopilador NO escribe directo al `app.md`; deja la propuesta para que el humano apruebe (similar a `proposals`).
### Adopcion gradual
- Apps SIN `e2e_checks` declarado: `fn doctor` muestra warning, no bloquea nada.
- Apps CON `e2e_checks`: `fn-analizador` corre la suite. Si critical falla → `fn-mejorador` crea proposal. Gate opcional en `/git-push`.
- Pilotos iniciales: `apps/kanban`, `projects/osint_graph/apps/graph_explorer`. Resto de apps van migrando segun necesidad.
### Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| `cmd: "make test"` con make-target opaco | Ilegible. El check debe ser ejecutable directo y auditable. |
| Check que tarda > 5 min sin razon (smoke pesado) | Bloquea iteracion. Mover a CI nocturno con tag `slow`. |
| Smoke que toca produccion | Riesgo. Smoke usa BD efimera, puertos altos, mocks. |
| `expect_stdout_contains: ""` | Vacio = siempre pass. No es un check. |
| Anidar checks (uno depende de side-effects de otro sin declararlo) | Frigil. Cada check arranca lo que necesita. |
| Usar `e2e_checks` como sustituto de tests unitarios | Son cosas distintas. Unit tests viven en `*_test.go`/`pytest`. e2e valida que el sistema arranque y haga su trabajo. |
### Tabla `e2e_runs` en operations.db
Cada corrida de `fn-analizador` se persiste:
```sql
CREATE TABLE IF NOT EXISTS e2e_runs (
id TEXT PRIMARY KEY,
app_id TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER,
status TEXT NOT NULL, -- pass|fail|partial
checks_total INTEGER NOT NULL,
checks_pass INTEGER NOT NULL,
checks_fail INTEGER NOT NULL,
summary_json TEXT NOT NULL
);
```
Migracion: `fn_operations/migrations/006_e2e_runs.sql` (issue 0068, paso 3).
### Output canonico de fn-analizador
Tabla caveman, una linea por check:
```
build ✓ 42s
smoke ✓ 0.8s
ops_audit ✓
tests ✗ 12s exit 1, 3/45 failures
assertion:R1 ✗ warning duration drift +47% vs p50
golden:home ✓
```
Rojo cuando `severity: critical` y status fail. Esto es lo que el agente principal lee y reenvia al humano.
+191
View File
@@ -0,0 +1,191 @@
## Feature flags: enviar codigo incompleto a master sin romperlo
Doctrina oficial de **trunk-based development**: master siempre desplegable. Cuando una feature no cabe en una sola rama corta, o cuando hay WIP que no esta terminado pero el resto si, **el codigo viaja detras de un flag OFF**. Asi master sigue verde y el codigo a medio terminar no llega a usuarios reales.
Refs: [trunkbaseddevelopment.com/feature-flags/](https://trunkbaseddevelopment.com/feature-flags/), [trunkbaseddevelopment.com/branch-by-abstraction/](https://trunkbaseddevelopment.com/branch-by-abstraction/).
### Cuando usar feature flag
| Situacion | Accion |
|---|---|
| Feature multi-issue (`0015a`, `0015b`, `0015c`) que llevan dias | Cada sub-issue mergea con flag OFF. Ultimo sub-issue activa flag. |
| Refactor grande tipo "Branch by Abstraction" (ej. cambiar driver DB) | Crear abstraccion + impl nueva con flag. Eliminar antigua + flag al final. |
| Cambio con riesgo en produccion que necesita rollback rapido | Flag para apagar sin redeploy. |
| Despliegue gradual (un PC primero, luego todos) | Flag por PC/usuario/grupo. |
| WIP detectado al cerrar otra rama | Envolver el codigo a medias en flag OFF, mergear, terminar despues. |
### Cuando NO usar feature flag
- Bug fix autocontenido → mergear directo, sin flag.
- Refactor que cabe en una rama corta → directo.
- Docs, comments, type signatures → directo.
- Codigo que no compila o no pasa tests → **NO viaja a master, ni con flag**. Flag protege codigo terminado, no roto.
### Flag != WIP
- **WIP**: codigo a medias, no compila o no testea. NO va a master.
- **Flag**: codigo terminado y testeado, pero no expuesto al usuario. SI va a master.
Si hay 80% terminado y 20% pendiente: completar al menos un slice vertical funcional (compila, pasa tests, se puede activar end-to-end), mergear con flag OFF, dejar el 20% para otra rama. NO mergear el 20% sin proteger.
### Archivo de flags
`dev/feature_flags.json` en la raiz del repo (registry o app). Formato canonico:
```json
{
"flags": {
"<flag-name>": {
"enabled": false,
"issue": "0063",
"description": "Descripcion 1 linea de la feature",
"added": "2026-05-08",
"enabled_at": null
}
}
}
```
Cuando se activa: cambiar `enabled: true` y rellenar `enabled_at` con fecha. Cuando la feature ya es estable y no necesita rollback (semanas/meses despues): borrar el flag y todas sus ramas condicionales del codigo. **Los flags caducan**; documentar fecha de revision para evitar que se acumulen.
### Patron por stack
#### Go (apps/services)
Cargar flags al arrancar. Patron simple — hashmap en memoria + helper `Enabled(name)`:
```go
// pkg/flags/flags.go (puro hasta donde se pueda)
package flags
import (
_ "embed"
"encoding/json"
)
type Flag struct {
Enabled bool `json:"enabled"`
Issue string `json:"issue"`
Description string `json:"description"`
}
type Flags struct{ Flags map[string]Flag `json:"flags"` }
func Parse(b []byte) (Flags, error) {
var f Flags
err := json.Unmarshal(b, &f)
return f, err
}
func (f Flags) Enabled(name string) bool {
flag, ok := f.Flags[name]
return ok && flag.Enabled
}
```
Uso:
```go
if flags.Enabled("kanban-stickers") {
registerStickerRoutes(router)
}
```
Para flags en frontend embebido: serializar a `/api/flags` y leer desde el cliente (ver TS).
#### TypeScript / React
Inyectar en build (Vite) o exponer endpoint `/api/flags`:
```ts
// src/flags.ts
let cache: Record<string, boolean> | null = null;
export async function loadFlags(): Promise<Record<string, boolean>> {
if (cache) return cache;
const res = await fetch("/api/flags");
const data = await res.json();
cache = Object.fromEntries(Object.entries(data.flags).map(([k, v]: [string, any]) => [k, !!v.enabled]));
return cache;
}
export function isEnabled(name: string): boolean {
return !!(cache?.[name]);
}
```
Render condicional:
```tsx
{isEnabled("kanban-stickers") && <StickerToolbar ... />}
```
Para flags en build-time (constantes del bundle), usar `import.meta.env.VITE_FLAG_X` o un plugin Vite que reemplace simbolos.
#### Bash / pipelines
Lectura directa con `jq`:
```bash
ENABLED=$(jq -r '.flags["my-feature"].enabled' dev/feature_flags.json)
if [ "$ENABLED" = "true" ]; then
run_new_path
else
run_legacy_path
fi
```
#### Python
```python
import json
from pathlib import Path
def flags() -> dict:
return json.loads(Path("dev/feature_flags.json").read_text())["flags"]
def enabled(name: str) -> bool:
f = flags().get(name)
return bool(f and f.get("enabled"))
if enabled("nuevo-pipeline"):
run_new()
else:
run_legacy()
```
### Branch by Abstraction (caso especial)
Para cambios grandes (ej. swap iBatis → Hibernate, swap libreria, swap protocolo):
1. **Abstraer**: crear interfaz que envuelve la implementacion antigua. Master sigue verde con la antigua. Mergear.
2. **Implementar nueva**: bajo la misma interfaz, detras de flag OFF. Tests para ambas. Mergear.
3. **Activar**: flip flag a ON en commit pequeño. Si rompe, flip OFF de inmediato.
4. **Eliminar antigua**: borrar codigo legacy + flag + abstraccion. Mergear.
Cada paso es un merge corto, master nunca esta roto, hay rollback en cada punto.
### Reglas operativas
- **Un flag = un proposito**. Si necesitas dos toggles independientes, usa dos flags.
- **Flag bool por defecto**. Si necesitas A/B/C, sigue siendo bool por nombre (`my-feature-v2`, `my-feature-v3`).
- **Tests con flag ON y OFF**. CI corre ambos paths cuando el flag toca codigo critico.
- **Documenta en el issue**: que flag protege que codigo, cuando se va a activar, cuando se va a borrar.
- **No anidar flags**. Si una rama esta detras de dos flags, simplifica.
- **Borra el flag**. Cuando la feature lleva semanas activa sin rollback, eliminar el flag es trabajo real, no opcional.
### Anti-patrones
| Anti-patron | Por que es malo |
|---|---|
| `if (flag) { ... } else { ... }` esparcido por 30 archivos | Imposible de borrar. Usar inyeccion / strategy pattern. |
| Flag que lleva 6 meses ON sin borrar | Deuda tecnica. Borrar el flag y simplificar. |
| Flag para WIP que no compila | Master roto. Eso no es flag, es WIP — no debe estar en master. |
| Flag condicional sobre tipos / esquemas DB | Migrations son irreversibles. No se "apaga" una columna. Usar branch-by-abstraction sobre la lectura/escritura, no sobre el schema. |
| Flag con nombre del autor o del issue (`lucas-experiment`, `flag-0063`) | Sin contexto al releerlo. Nombrarlo por la feature: `kanban-stickers`. |
### Comandos relacionados
- `/git-branch` — crea rama desde master.
- `/git-push` — merge --no-ff + push.
- Para registrar / activar un flag: editar `dev/feature_flags.json` directamente y commitear con el codigo correspondiente. No hay CLI dedicada todavia.
+78
View File
@@ -0,0 +1,78 @@
## fn doctor: diagnostico del registry y artefactos
`fn doctor` es el entrypoint unico para auditar la salud del sistema de forma read-only. Compone funciones del registry (`functions/infra/`) y formatea su salida. No modifica nada.
### Cuando usar
- Despues de un deploy: confirmar que servicios siguen vivos y artefactos intactos.
- Despues de `git pull` o `fn sync`: detectar drift entre BD y disco.
- Antes de `fn index` masivo: confirmar que apps Go/Py siguen declarando bien sus deps.
- Periodicamente (cron): listar funciones del registry sin consumidores para limpiar.
- Como gate antes de crear proposals: si `fn doctor` esta verde, las metricas del bucle reactivo son fiables.
### Comandos
```bash
fn doctor # Corre TODOS los checks (artefacts + services + sync + uses-functions + unused + cpp-apps)
fn doctor artefacts # Solo artefactos: git/venv/app.md/upstream
fn doctor services # Solo apps con tag 'service' + systemctl + puerto
fn doctor sync # Solo drift pc_locations BD vs disco local
fn doctor uses-functions # Solo audit imports reales vs uses_functions
fn doctor unused # Solo funciones huerfanas del registry
fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado)
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
```
### Mapeo subcomando → funcion del registry
| Subcomando | Funcion |
|---|---|
| `artefacts` | `artefact_doctor_go_infra` |
| `services` | `services_status_go_infra` |
| `sync` | `pc_locations_drift_go_infra` |
| `uses-functions` | `audit_uses_functions_go_infra` |
| `unused` | `find_unused_functions_go_infra` |
| `cpp-apps` | `audit_cpp_apps_go_infra` |
Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente.
### Salida
Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable para `jq`, agentes o pipes.
### Idempotente y seguro
- Read-only: ningun subcomando escribe, mata procesos ni cambia estado.
- `services` abre conexiones TCP a `127.0.0.1:<port>` con timeout 500ms — no genera trafico saliente.
- `artefacts` ejecuta `git rev-parse @{u}` con timeout 3s por artefacto.
### Acciones complementarias (NO son `fn doctor`)
`fn doctor` solo diagnostica. Las acciones derivadas son verbos separados:
| Si `fn doctor` reporta... | Accion |
|---|---|
| `directory_missing` | Marcar `pc_locations.status='missing'` o re-clonar via `/full-git-pull` |
| `git_not_initialized` | `gitea_create_repo_bash_infra` + `ensure_repo_synced_bash_infra` |
| `venv_broken_path` | `cd <analysis_dir> && rm -rf .venv && uv sync` |
| `service active=inactive` | `systemctl --user start <unit>` o investigar logs |
| `port not listening` | `port_kill_bash_infra <port>` (si zombie) y relanzar |
| `missing_in_app_md` | Editar `app.md` y añadir el ID a `uses_functions` |
| `unused` (funcion huerfana) | Decidir: usar, deprecar (tag), o borrar |
| `manual_app_menubar_call` | Borrar `fn_ui::app_menubar(...)` del render — el framework ya lo dibuja |
| `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio |
| `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` |
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
### Para agentes
Patron recomendado tras una accion no trivial (deploy, sync, mass edit):
```bash
fn doctor --json > /tmp/doctor.json
# Agente parsea JSON, decide si crear proposals o avisar al humano
```
Si el agente quiere actuar sobre los hallazgos, abre proposals con `fn proposal add` referenciando los IDs afectados — NO toca artefactos directamente sin aprobacion humana.
+27
View File
@@ -0,0 +1,27 @@
En todos los frontends se usan los componentes de `@fn_library` (alias a `frontend/functions/ui/`) antes que elementos HTML nativos o librerias externas.
El sistema de UI es Mantine v9. Todos los componentes de @fn_library wrappean componentes de Mantine.
**Theming:** Cada app define su tema con `createTheme()` de `@mantine/core` y lo pasa a `MantineProvider` (o `FnMantineProvider` de @fn_library). No se usan CSS variables custom — Mantine genera las suyas automaticamente (`--mantine-color-*`).
**Styling:** No se usa Tailwind, CVA, cn(), ni clases CSS manuales. Los componentes se estilizan con props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, etc.) y el style system de Mantine.
**Iconos:** Se usa `@tabler/icons-react` (el set nativo de Mantine), no lucide-react.
**Layout:** Se usan los componentes de layout de Mantine: `Group`, `Stack`, `Grid`, `Flex`, `SimpleGrid`, `AppShell`, `Container`, `Box`, `Paper`.
**AppShell.Navbar / AppShell.Aside (gotchas v9):**
- NO override `position` via `style` (ej. `style={{ position: "relative" }}`). Mantine aplica `position: fixed` con CSS class; si lo pisas, el slot cae al flow normal y empuja el resto del layout abajo (root altura 2x).
- Para anclar children `position: absolute` (drag handle, badge flotante), el `position: fixed` del propio slot ya actua como containing block — no necesitas relative.
- Por defecto el navbar **empuja** el main (anade `padding-inline-start: navbar-width`). Para **overlay** (navbar tapa main):
```tsx
<AppShell styles={{ main: { paddingInlineStart: 0 } }}>
```
Idem `paddingInlineEnd: 0` para aside overlay.
- Si quieres backdrop dimming + click-outside-close: usa `<Drawer position="left">` en lugar de `AppShell.Navbar`.
- **Memoizar configs**: `header`/`navbar`/`aside`/`styles` aceptan objetos. Si el componente padre se re-renderiza cada N (tick, ws, etc.), los objetos literales se recrean y Mantine regenera el `<style>` inline. Wrap con `useMemo([deps])`:
```tsx
const navbarCfg = useMemo(() => ({ width, breakpoint: "md", collapsed: { ... } }), [width, navOpen]);
<AppShell navbar={navbarCfg} ...>
```
@@ -0,0 +1,115 @@
## Function growth + self-documenting capability
Dos doctrinas hermanas. Una define **como deben ser** las funciones (auto-descubribles y lanzables sin segunda lectura). La otra define **como crece** el registry (no inflando funciones — promoviendo composiciones a pipelines).
Issue 0087.
---
### Parte A — `.md` autosuficiente (contrato OBLIGATORIO)
Cuando Claude (o un humano) encuentra una funcion via FTS / fuzzy match / capability page / TOP block, el `.md` debe bastar para **lanzarla sin abrir el codigo**. Esto es lo que hace que descubrir = lanzar y elimina el coste del second lookup.
**Secciones obligatorias** en cada `.md` del registry (functions + pipelines + types con uso practico):
| Seccion | Contenido | Tamaño |
|---|---|---|
| Frontmatter | `name`, `signature`, `params` (con `desc` por param), `output`, `tags`, `uses_functions`, etc. Lo de hoy. | — |
| `## Ejemplo` | Bloque de codigo lanzable con args **concretos**. Copiar+pegar produce ejecucion real. NO placeholders abstractos. | 3-10 lineas |
| `## Cuando usarla` | 1-2 frases con triggers: "cuando hagas X / antes de Y / si necesitas Z". Verbos imperativos. Ayuda al fuzzy match y a Claude a saber sin leer el codigo. | 1-3 lineas |
| `## Gotchas` | Problemas conocidos / no-go cases. Obligatoria para funciones impuras o con efectos (Windows-side, red, FS write, GPU). Omisible para funciones puras triviales. | 0-5 puntos |
| `## Capability growth log` | Solo SI la funcion ha crecido. Una linea por version: `v1.1.0 (YYYY-MM-DD) — anade --build flag para skip build`. No se rellena en v1.0.0. | crece con el tiempo |
**Anti-patrones del .md:**
- Ejemplo con `<arg1>`, `<arg2>` placeholders abstractos — NO. Ejemplos con valores reales (`registry_dashboard`, `/home/lucas/...`).
- "Cuando usarla" vacio o "ver descripcion arriba" — NO. Frase nueva con trigger explicito.
- `notes` lleno + `## Gotchas` vacio cuando la funcion tiene efectos — mover de `notes` a `## Gotchas`.
- Capability growth log inventado (sin que la funcion haya cambiado) — NO. Solo se rellena cuando hay version bump real.
**Verificacion** (TBD: convertir a check de `fn doctor`): cada .md de `functions/`/`pipelines/` debe tener `## Ejemplo` y `## Cuando usarla`. `## Gotchas` obligatoria solo si `purity: impure`. `## Capability growth log` libre.
---
### Parte B — Crecimiento por composicion (no por inflado)
**Principio:** una funcion que hace bien UNA cosa NO necesita crecer. Anadir params "por si acaso" la hace peor (Inner Platform Effect). Lo que crece es el **registry**: pipelines nuevos que componen funciones existentes.
#### Ejemplo del principio
- **Hoy:** Claude para hacer una transferencia bancaria llama `bank_login` -> `bank_list_accounts` -> `bank_make_transfer`. 3 calls, 3 decisiones, 3 puntos de fallo.
- **Manana:** pipeline `bank_transfer_oneshot(account, amount, target)` que compone las 3 internamente. 1 call, 1 decision.
Misma capacidad, 3x menos pasos. **Esto es lo que multiplica la velocidad de Claude**, no anadir flags a `bank_login`.
#### Como se promueve una composicion
Senal detectable en `call_monitor.operations.db`: secuencia A→B(→C) con
- **Mismo session_id**.
- **Intervalo entre calls < N segundos** (default 30s).
- **Occurrences > K** (default 5) en ventana de **D dias** (default 30).
- **Success rate > S** (default 0.9 — falla < 10%).
- **No existe ya un pipeline** que la cubra (validar con FTS sobre `uses_functions`).
Cuando se cumple → **proposal `new_pipeline`** con evidencia (sequence_ids, session_ids, occurrence count). Humano (o `fn-orquestador` autonomo) decide promover.
#### Implementacion (issue 0087 tanda A)
- `call_monitor sequences --detect` subcomando: escanea `calls` table, agrupa por session+window, computa secuencias, upserta en tabla `function_sequences`.
- Cron diario que ejecuta el detector + genera proposals automaticas.
- Visible en Monitor tab del `registry_dashboard`: sub-tab "Promotion candidates".
#### Cuando SI inflar una funcion
Casos legitimos para anadir feature a una funcion existente:
1. **Generalizar firma** sin romper consumidores (anadir param opcional con default sensato).
2. **Mejor manejo de error** (mensajes mas claros, retry sensible).
3. **Default mas inteligente** (autodetectar lo que antes era arg obligatorio).
4. **Eliminar gotcha conocido** (fix de bug que estaba en `## Gotchas`).
NO infles para casos hipoteticos. NO anadas params "por flexibilidad". Si dudas, separa la responsabilidad en una funcion nueva o un pipeline.
#### Capability growth log — cuando se rellena
- Se rellena **solo cuando la funcion crece** (alguno de los 4 casos arriba).
- Cada bump de `version` -> 1 linea en `## Capability growth log` con fecha y resumen 1-frase.
- Una funcion estable de hace 6 meses puede seguir en v1.0.0 sin log: indica madurez, no abandono.
- Telemetria (call_monitor) decide si una funcion estable es huerfana (`calls_90d=0`) o usada-y-buena (`calls_30d>10, error_rate<0.05`). Las primeras se deprecan; las segundas se respetan.
---
### Parte C — Output de discovery
Cuando un mecanismo de discovery (fuzzy match / FRESH hook / TOP block / capability page) surfacea una funcion, el payload **minimo** es:
```
<id> → <signature> → <ejemplo de 1 linea>
```
Ejemplo concreto:
```
redeploy_cpp_app_windows_bash_pipelines
./fn run redeploy_cpp_app_windows registry_dashboard /path/to/app [--build]
use: tras compilar cpp/build/windows, antes de smoke test manual
```
Si Claude necesita mas (gotchas, params completos, codigo), un `mcp__registry__fn_show <id>` adicional. Pero el primer hit ya basta para el 80% de casos.
---
### Parte D — Relacion con otras reglas
- [[registry_first]] dice CUANDO buscar/usar/delegar. Esta regla dice **COMO** debe ser la funcion para que esa busqueda valga.
- [[ids_naming]] hace ID predictible. Esta regla hace metadata predictible.
- [[delegation]] dice cuando spawnar fn-constructor. Esta regla es lo que fn-constructor debe producir.
- [[capability_groups]] agrupa funciones afines. Las paginas madre de cada grupo deben respetar el mismo contrato self-doc (mejor con su propio ejemplo end-to-end por grupo).
### Resumen TL;DR
1. Cada `.md` autosuficiente: Ejemplo + Cuando usarla + Gotchas (si impura) + Growth log (si crecio).
2. Las funciones que hacen bien una cosa NO necesitan crecer.
3. El registry crece **promoviendo composiciones repetidas a pipelines**, no inflando funciones.
4. Telemetria de `call_monitor` detecta secuencias candidatas y abre proposals automaticas.
5. Discovery devuelve siempre: `id + signature + 1-line example`. Resto on-demand.
+30
View File
@@ -0,0 +1,30 @@
Los pipelines con tag `launcher` aparecen en el Pipeline Launcher TUI (`apps/pipeline_launcher`).
Sin el tag, el pipeline no es lanzable desde la TUI. Añadir `launcher` al array `tags` del .md al crear un pipeline ejecutable desde el launcher.
Pipelines interactivos (TUIs) o que no son subprocesos NO deben llevar este tag.
## Tag `service`
Las apps con tag `service` son procesos de larga duracion: APIs, daemons, watchers, servers.
Diferencia con una app normal:
- Una **app** se ejecuta, hace su trabajo, y termina (CLI, TUI, script)
- Un **service** se lanza y queda corriendo indefinidamente (API server, scheduler, watcher)
Añadir `service` al array `tags` del `app.md` cuando la app esta diseñada para correr como proceso persistente.
Un service sigue siendo una app — vive en `apps/`, tiene `app.md`, se indexa igual. El tag es solo metadata para filtrar:
```sql
-- Listar services
SELECT id, name, description FROM apps WHERE tags LIKE '%service%';
-- Listar apps que NO son services
SELECT id, name, description FROM apps WHERE tags NOT LIKE '%service%';
```
Documentar en el `app.md` del service:
- El puerto que usa (si expone HTTP/gRPC)
- Como lanzarlo y pararlo
- Como comprobar que esta vivo (health check)
+30
View File
@@ -0,0 +1,30 @@
## KISS en proyectos y apps
**Mantener proyectos (`projects/`) y apps (`apps/`, `projects/*/apps/`) simples**. La complejidad no solicitada es deuda — cada línea, cada dependencia y cada herramienta externa se justifican o no entran.
### Reglas
1. **Preferir herramientas ya presentes en el sistema o en el registry** antes que paquetes/CLI externos.
- ¿Lo hace `git` / `bash` / una función del registry? Úsalo.
- Antes de añadir una dependencia nueva, buscar en `registry.db` (FTS5) si ya existe algo similar.
2. **Cuestionar cada nueva herramienta externa**. Antes de instalarla preguntar:
- ¿Qué problema concreto resuelve que NO podemos resolver con lo que ya tenemos?
- ¿El coste (instalar, mantener, aprender, conflictos con nuestro flujo) compensa el beneficio real?
- ¿Qué pasa si el proyecto upstream se abandona / rompe compatibilidad?
3. **Sin abstracciones ni features especulativas**. No generalizar "por si acaso". Tres líneas similares son mejores que una abstracción prematura.
4. **Ser consciente del flujo de trabajo actual**. Si algo funciona bien con `git` / submódulos / `fn` CLI, no lo sustituyas por una herramienta que prometa "mejorarlo" sin evidencia de mejora concreta en tu contexto.
5. **Escritura de apps**: una responsabilidad clara, layout mínimo (`main.*`, `app.md`, y lo estrictamente necesario), sin config ni estructuras que no se usen hoy.
### Caso aprendido (GitButler)
Se probó GitButler (virtual branches) pensando en paralelizar trabajo. Resultado:
- Bugs con submódulos (git submodule add + gitlinks) — commits vacíos o contenido cruzado.
- Auto-commits con el texto del chat como commit message.
- Pre-commit hook que bloquea `git commit` directo y exige otro CLI (`but`).
- Un binario externo de 37 MB + un plugin en Claude Code + skill propio + hooks en `settings.json`.
Al volver a `git` + ramas normales + `fn` CLI: cero fricción, commits limpios, submódulos funcionan. **Lección**: antes de adoptar una capa nueva, medir la fricción real actual. Si no la hay, no vale la pena añadir complejidad.
+55
View File
@@ -0,0 +1,55 @@
## Colaboración en notebooks Jupyter
### Requisito previo
El usuario debe tener Jupyter Lab corriendo en modo colaborativo (`--collaborative`) y el notebook abierto en el browser. Sin esto, los cambios no se ven en tiempo real.
El launcher estándar (`run-jupyter-lab.sh` generado por `init_jupyter_analysis`) ya incluye `--collaborative`.
### Funciones del registry (dominio `notebook`)
| Función | ID | Para qué |
|---|---|---|
| `jupyter_discover` | `jupyter_discover_py_notebook` | Descubrir instancias Jupyter activas, kernels, sesiones, modo colaborativo |
| `jupyter_read` | `jupyter_read_py_notebook` | Leer celdas (todas o una), metadata del notebook |
| `jupyter_exec` | `jupyter_exec_py_notebook` | Ejecutar: append+execute, execute celda existente, o directo al kernel |
| `jupyter_write` | `jupyter_write_py_notebook` | Escribir: append code/markdown, insert, edit, delete celdas |
| `jupyter_kernel` | `jupyter_kernel_py_notebook` | CRUD de kernels: list, start, restart, interrupt, shutdown, sessions |
### Invocación desde cualquier sesión de Claude
```bash
PYTHON="python/.venv/bin/python3"
# 1. Descubrir qué Jupyter está corriendo
$PYTHON python/functions/notebook/jupyter_discover.py --json
# 2. Leer notebook
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --json
# 3. Añadir celda y ejecutar (el usuario la ve en tiempo real)
$PYTHON python/functions/notebook/jupyter_exec.py append notebooks/01.ipynb "df.describe()"
# 4. Ejecutar celda existente
$PYTHON python/functions/notebook/jupyter_exec.py cell notebooks/01.ipynb 3
# 5. Ejecutar en kernel sin tocar notebook
$PYTHON python/functions/notebook/jupyter_exec.py kernel "print(df.shape)"
# 6. Añadir markdown
$PYTHON python/functions/notebook/jupyter_write.py append-markdown notebooks/01.ipynb "## Resumen"
# 7. Gestionar kernels
$PYTHON python/functions/notebook/jupyter_kernel.py list
$PYTHON python/functions/notebook/jupyter_kernel.py sessions
$PYTHON python/functions/notebook/jupyter_kernel.py shutdown <kernel_id>
```
### Reglas de uso
- **SIEMPRE** ejecutar `jupyter_discover` primero para confirmar que Jupyter está activo y el notebook abierto.
- Las funciones resuelven automáticamente el `kernel_id` de la sesión del notebook y el `username` colaborativo via `/api/sessions` y `/api/me`.
- Después de escribir/ejecutar, las funciones mantienen la conexión WebSocket 2 segundos para que Y.js propague los cambios al browser.
- **NO usar MCP jupyter** — estas funciones reemplazan al MCP y funcionan desde cualquier directorio sin registrar nada.
- El token por defecto es vacío (sin auth). Si el server tiene token, pasarlo con `--token`.
- Los paths de notebooks son relativos a la raíz del servidor Jupyter (normalmente `analysis/{tema}/`).
+58
View File
@@ -0,0 +1,58 @@
## Playgrounds: prototipos rapidos dentro de un artefacto
Un **playground** es un mini-artefacto efimero que vive **dentro** de otro artefacto (analysis, app o project) y reutiliza su entorno. Sirve para probar visualmente una idea (webapp, demo, dashboard, ejercicio interactivo) antes de decidir si se promueve a app independiente.
Ejemplo canonico: `projects/osint_graph/analysis/gliner_glirel_tuning/playground/` — server FastAPI + index.html + JS vendored que reutiliza el `.venv` del analisis padre para visualizar las recetas del notebook 08 con UI interactiva.
### Estructura
```
<artefacto_padre>/
playground/ # Un solo playground por padre (si necesitas mas, usa subdirs)
server.py | server.go | ... # Punto de entrada (single-file preferido)
index.html # UI si la hay
static/ # JS/CSS vendored (no node_modules ni pnpm)
server.log # Log local (gitignorable)
```
Si el playground crece a varios subdirs/modulos, ya no es playground — promover a app.
### Reglas
1. **Hereda el entorno del padre**. NO crea su propio `.venv`, `package.json`, ni dependencias. Si el padre es un analysis Python, usa `../.venv/bin/python3`. Si el padre es una app Go, comparte el `go.mod`.
2. **NO se indexa**. No tiene `app.md`, no aparece en `registry.db`, no tiene entrada en `pc_locations`.
3. **NO tiene repo propio**. Vive dentro del repo Gitea del artefacto padre y se mueve con el.
4. **Single-file preferido**. Un `server.py` o `main.go` con todo dentro. Si hace falta partir, considera promover a app.
5. **Vendor deps front**. JS/CSS como `.min.js` en `static/`, sin `node_modules`. Si necesitas pnpm/vite, ya no es playground.
6. **Reutiliza funciones del registry** igual que el padre — `sys.path` al `python/functions`, importar paquetes, etc.
7. **Ciclo de vida**: vive mientras la idea esta cruda. Una vez probada, dos caminos:
- **Promover a app** (extraer logica reutilizable como funciones del registry, crear `app.md`, mover a `apps/`).
- **Borrar** sin contemplaciones si el experimento no llevo a nada.
### Cuando NO usar playground
- Si necesitas correr en un VPS / tener systemd / health check → es un app + service, no playground.
- Si la idea ya esta clara y el codigo va a sobrevivir meses → arrancar como app desde el primer dia ahorra una migracion.
- Si necesitas operations.db, assertions, o el bucle reactivo → es app.
- Si el padre seria un proyecto entero solo para contener el playground → probablemente sea app standalone con `tags: [prototype]`.
### Relacion con `temp/`
| Cuando | Donde |
|---|---|
| Idea suelta sin contexto, prueba de API, snippet desechable | `temp/<lo_que_sea>/` (gitignored, sin contexto) |
| Prototipo ligado a un analysis/app/proyecto que reutiliza su entorno | `<padre>/playground/` (versionado con el padre) |
| Codigo que sobrevive y se reutiliza en otros sitios | extraer a `functions/` |
| Aplicacion ejecutable con identidad propia | `apps/` o `projects/<p>/apps/<a>/` |
`temp/` es para cosas sin padre. Playground es para cosas con padre. Si dudas entre los dos, empieza en `temp/` y mueve a `playground/` cuando quede claro de que artefacto depende.
### Lanzar un playground
Sin convencion fija — depende del stack. El propio `server.py` o un README en el playground documenta como arrancarlo. Ejemplo del playground de OSINT:
```bash
cd projects/osint_graph/analysis/gliner_glirel_tuning/playground
../.venv/bin/python3 server.py
# http://localhost:7878
```
+88
View File
@@ -0,0 +1,88 @@
## Projects: apps, analysis y vaults bajo un tema comun
Un project agrupa apps, analyses y vaults relacionados. Vive en `projects/{nombre}/` con esta estructura:
```
projects/{nombre}/
project.md # Frontmatter obligatorio (name, description, tags)
apps/ # Apps del proyecto (cada una con app.md)
{app_name}/
app.md
...
analysis/ # Analyses del proyecto (cada uno con analysis.md)
{analysis_name}/
analysis.md
.venv/
notebooks/
run-jupyter-lab.sh
...
vaults/ # Datos del proyecto
vault.yaml # Manifest de vaults (nombre, descripcion, path, tags)
{vault_name} -> /abs/path # Symlinks a directorios reales de datos
```
### Reglas
- `project.md` sigue el template de `docs/templates/project.md` — campos: `name`, `description`, `tags`, `repo_url`
- `analysis.md` sigue el template de `docs/templates/analysis.md``dir_path` debe apuntar a `projects/{nombre}/analysis/{tema}/`
- `vault.yaml` lista los vaults con nombre, descripcion, path absoluto y tags
- Los vaults reales viven fuera del repo (ej: `~/vaults/{nombre}/`) con symlinks en el proyecto
- `fn index` escanea `projects/*/` y setea `project_id` automaticamente en apps, analyses y vaults
- Apps y analyses sueltos (sin proyecto) siguen en `apps/` y `analysis/` en la raiz
### Raiz vs proyecto
| Ubicacion | Para que |
|-----------|---------|
| `apps/` | Apps independientes que no pertenecen a ningun proyecto |
| `analysis/` | Analyses independientes |
| `projects/{nombre}/apps/` | Apps de un proyecto — `project_id` se setea automaticamente |
| `projects/{nombre}/analysis/` | Analyses de un proyecto — `project_id` se setea automaticamente |
### Crear un proyecto nuevo
```bash
# 1. Crear estructura
mkdir -p projects/{nombre}/{apps,analysis,vaults}
# 2. Crear project.md con frontmatter
fn add -k project # genera template
# 3. Crear vault (datos fuera del repo, symlink dentro)
mkdir -p ~/vaults/{vault_name}/{raw,processed,exports}
ln -s ~/vaults/{vault_name} projects/{nombre}/vaults/{vault_name}
# Crear vault.yaml con la entrada
# 4. Crear analysis dentro del proyecto (un solo comando; ya indexa)
fn run init_jupyter_analysis --project {nombre} {nombre_analysis} --desc "..." [paquetes...]
# 5. Verificar
fn show {nombre} # verifica el project y sus componentes
# NUNCA: crear el analisis en analysis/ y luego mv al proyecto.
# Al mover se rompe el .venv (paths hardcodeados en activate).
# Si ya te paso: cd projects/{nombre}/analysis/{tema} && rm -rf .venv && uv sync
```
### Consultas utiles
```sql
-- Listar proyectos
SELECT id, description FROM projects;
-- Analysis de un proyecto
SELECT id, name, dir_path FROM analysis WHERE project_id = 'app_turismo';
-- Vaults de un proyecto
SELECT id, name, path, symlink FROM vaults WHERE project_id = 'app_turismo';
-- Apps de un proyecto
SELECT id, name, dir_path FROM apps WHERE project_id = 'app_turismo';
-- Todo lo que pertenece a un proyecto
SELECT 'analysis' as tipo, id, name FROM analysis WHERE project_id = ?
UNION ALL
SELECT 'vault', id, name FROM vaults WHERE project_id = ?
UNION ALL
SELECT 'app', id, name FROM apps WHERE project_id = ?;
```
+147
View File
@@ -0,0 +1,147 @@
## Como invocar funciones del registry — patrones canonicos
Toda invocacion del agente al registry sigue uno de **tres patrones**. Cualquier otro patron es antipatron auditable. Las invocaciones se loguean en `projects/fn_monitoring/apps/call_monitor/operations.db` (issue 0085) para alimentar el bucle reactivo.
### Patrones canonicos
| Caso | Patron | Cuando |
|---|---|---|
| **Inspeccionar** (buscar, leer codigo, ver dependencias, listar dominios, leer proposals) | `mcp__registry__fn_search` / `fn_show` / `fn_code` / `fn_uses` / `fn_list_domains` / `fn_proposal` | SIEMPRE para descubrimiento, lectura de codigo, exploracion. |
| **Ejecutar** UNA funcion/pipeline con sus args | `mcp__registry__fn_run <id> [args]` (preferido) o `./fn run <id> [args]` (fallback CLI) | ID conocido + args planos. Despacho automatico por lenguaje. |
| **Componer** ad-hoc multi-funcion con logica intermedia | Heredoc `python/.venv/bin/python3 - <<'PYEOF' ... PYEOF` IMPORTANDO funciones del registry | Solo si hay loops/conditionals/dispatch entre N funciones. Las funciones del registry **se importan**, no se reescriben. |
### Antipatrones prohibidos (audit-targeted)
| Patron | Razon | Sustituir por |
|---|---|---|
| `sqlite3 registry.db "SELECT ..."` para buscar funciones/tipos | Salta MCP, FTS5 gotchas, sin trazabilidad | `mcp__registry__fn_search` |
| `sqlite3 registry.db "SELECT ... FROM proposals"` | Mismo problema | `mcp__registry__fn_proposal` |
| `python -c "import metabase; dir(metabase)"` para descubrir helpers | Fuente de verdad = registry, no `__init__.py` | `mcp__registry__fn_search "metabase"` + `mcp__registry__fn_show <id>` |
| Heredoc que reescribe logica que ya existe como funcion del registry | Reinvento + perdida de capitalizacion | Buscar primero; si falta, delegar a `fn-constructor` (no escribir inline) |
| `client._http.request(...)` saltando wrapper del registry | Salta validacion del wrapper y telemetria | Usar wrapper; si firma incompleta, `fn proposal add --kind improve_function` |
| Scripts en `temp/` para composiciones que se repiten >2 veces | Codigo perdido + sin monitoreo | Pipeline en `python/functions/pipelines/` o `bash/functions/pipelines/` |
| `from <pkg> import *` en heredoc | Imposible identificar funciones usadas | Imports explicitos `from <domain> import <name1>, <name2>` |
### Excepciones autorizadas para `sqlite3` directo
Casos donde el MCP no aplica y `sqlite3 registry.db` es legitimo:
- Introspeccion de schema: `.schema`, `.tables`, `PRAGMA table_info(...)`, `PRAGMA index_list(...)`.
- Agregaciones: `COUNT(*)`, `GROUP BY`, `SUM(...)`, `AVG(...)`.
- JOINs custom entre tablas que el MCP no expone (`functions JOIN unit_tests ON ...`).
- Columnas que el MCP no devuelve (rare; preferir proponer ampliacion del MCP).
El hook `PreToolUse` (`.claude/scripts/hook_registry_mcp.sh`) ya deja pasar estas excepciones y solo avisa cuando ve `sqlite3 registry.db "SELECT ..."` plano.
### Excepcion: hooks e infraestructura de telemetria (issue 0087)
Los **hooks** (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, etc.) y los **binarios de infraestructura** que sirven al agente (`fn_match`, `fn doctor`, `call_monitor`) **pueden leer `registry.db` directo** via `sqlite3` o `database/sql` con conexion read-only. NO estan sujetos a la regla MCP-first porque:
- No son acciones del agente — son inspeccion automatizada del entorno.
- El MCP requiere tool invocation por Claude; un hook no puede invocar tools.
- Latencia objetivo (50-200ms) incompatible con round-trip MCP.
**Restricciones:**
- SOLO lectura. Conexion debe abrirse con `?mode=ro` o `?_query_only=1`.
- NUNCA escritura a `registry.db` desde hooks.
- Si un hook necesita escribir (cache, telemetria propia), usa su propia DB (`operations.db` del app de hooks, o `~/.fn_hooks/cache.db`).
Esta excepcion es **explicita y acotada** — no aplica al agente, que sigue regido por la regla MCP-first.
### Verificacion previa — `fn doctor`
Antes de empezar trabajo no trivial sobre el registry, ejecutar `fn doctor` para confirmar que el ecosistema esta sano:
- Artefactos OK (sin `git_not_initialized`, `venv_broken_path`, etc.).
- Services activos cuando se necesiten (`sqlite_api`, `registry_api`, `registry_mcp`).
- Sin drift `pc_locations` vs disco.
- Sin drift `uses_functions` vs imports reales.
Si `fn doctor` reporta `service inactive` para `registry_mcp.service`, el MCP estara siendo invocado en modo stdio por Claude Code (normal); el systemd unit solo aplica al modo HTTP. Si el binario no responde, rebuild: `cd apps/registry_mcp && CGO_ENABLED=1 go build -tags fts5 -o registry_mcp .`.
### Tools MCP disponibles
| Tool | Lectura/escritura | Gating |
|---|---|---|
| `fn_search` | read | siempre on |
| `fn_show` | read | siempre on |
| `fn_code` | read | siempre on |
| `fn_uses` | read | siempre on |
| `fn_list_domains` | read | siempre on |
| `fn_proposal` | read | siempre on |
| `fn_doctor` | read | siempre on |
| `fn_run` | execute (mutating side-effects) | requiere `--enable-run` |
| `fn_create_function` | write | requiere `--enable-write` |
### Heredoc Python — convenciones obligatorias
Cuando el caso 3 (composicion) sea inevitable:
1. **Imports explicitos** desde paquetes del registry. Nunca `import *`.
2. **No reescribir** la firma de una funcion del registry — importarla.
3. **Args via env vars o stdin JSON**, nunca interpolacion shell directa (inyeccion).
4. **Output a stdout JSON** cuando vaya a ser consumido por el siguiente paso.
5. **Si el heredoc supera ~30 lineas**, extraer a `python/functions/pipelines/`. El monitor avisara automaticamente cuando un patron similar se repita >5 veces.
### Trazabilidad — bucle reactivo
Cada evento alimenta a `call_monitor.db` (event-log append-only) y se rollupea en una vista `function_stats` con contadores por funcion del registry. Tablas event-log:
| Tabla | Captura |
|---|---|
| `calls` | Cada invocacion (heredoc/mcp/fn_run): function_id, tool_used, duration_ms, success, error_class, args_hash |
| `code_writes` | Cada Edit/Write sobre archivo del registry: function_id, session_id, lines_added/removed |
| `test_runs` | Cada `go test`/`pytest` que toca codigo del registry: function_id, test_id, passed, duration_ms |
| `e2e_runs_fn` | Cada check `e2e_checks` de app que usa la funcion: function_id, app_id, check_id, passed |
| `violations` | Antipatron detectado: rule_id, session_id, command_snippet, severity |
| `patterns` | Heredocs clusterizados: pattern_hash, session_ids[], occurrences, representative_snippet |
| `sessions` | session_id, cwd, started_at, ended_at, health_score, mcp_ratio |
Vista agregada `function_stats` por `function_id`:
- **Uso:** `calls_total`, `calls_24h/7d/30d/90d`, `last_used_at`
- **Errores:** `errors_total`, `error_rate`, `last_error_class`, `last_error_ts`
- **Performance:** `mean_duration_ms`, `p95_duration_ms`
- **Codigo:** `writes_count`, `last_write_at`
- **Tests:** `tests_total`, `tests_failed`, `test_fail_rate`, `last_test_failed_at`
- **E2E:** `e2e_total`, `e2e_failed`, `e2e_fail_rate`, `consumer_apps_count`
- **Salud:** `violations_caused`
Assertions derivadas → proposals automaticas:
| Regla | Threshold | Proposal |
|---|---|---|
| Huerfana absoluta | `calls_90d=0 AND writes_count=0` | `deprecate_function` |
| Bug prioritario | `error_rate>0.1 AND calls_7d>5` | `improve_function` (bug) |
| Regresion performance | `p95_24h > 1.5 * p95_30d` | `improve_function` (perf) |
| Test flaky | `test_fail_rate>0.1 AND tests_total>10` | `improve_function` (flaky) |
| Wrapper saltado | `violations_caused>3` | `improve_function` (API gap) |
| Patron inline sin funcion | `patterns.occurrences>5 AND no match FTS` | `new_function` con snippet |
| Blast radius alto | `e2e_fail_rate>0 AND consumer_apps_count>=3` | `improve_function` (critical) |
Datos sensibles: solo `args_hash`, NUNCA valores concretos. Snippets de error redactados via allowlist.
### Capas de monitorizacion (issue 0085)
Cobertura por capa, no todas activas a la vez:
| # | Capa | Activacion | Cobertura |
|---|---|---|---|
| 1 | Hook PostToolUse Bash | siempre (settings.local.json) | mcp, fn_cli_run, edit_registry, violations |
| 2 | Wrapper Python `registry_telemetry` | `FN_TELEMETRY=1` env var | heredocs + notebooks Jupyter |
| 3 | Wrapper Bash `telemetry_prelude.sh` | `source` explicito o `FN_TELEMETRY=1` | heredoc bash + apps bash |
| 4 | Interceptor en `fn run` | siempre (binario Go) | duration/error real de invocacion CLI |
| 5 | `fn doctor copied-code` | comando manual / cron | drift estatico: codigo copiado en apps |
| 6 | `function_versions` + snapshot | poblado por `fn index` + edit-hook | historial de versiones |
| 7-8 | Build-tag Go / macro C++ | opt-in por app | runtime de app (futuro) |
**Boundary:** monitorizamos al **agente** y a **invocaciones canonicas**. Runtime de apps Go/C++ compiladas queda fuera. Compensar con tests + `e2e_checks` (issue 0068).
### Que NO se monitoriza
- Funcion Go/C++ llamada internamente por app ya compilada.
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
- Service de produccion recibiendo HTTP.
**Implicacion:** una funcion con `calls_90d=0` puede ser huerfana real O usada en runtime invisible. Antes de proponer `deprecate_function`, cruzar con `consumer_apps_count > 0` (e2e) o con `fn doctor uses-functions` (declaraciones estaticas).
+50
View File
@@ -0,0 +1,50 @@
## Registry-first: reutilizar antes que escribir, delegar antes que escribir inline
**OBLIGATORIO para todos los artefactos** (apps, analyses, projects, playgrounds, services). El registry existe para que las apps se compongan a partir de funciones probadas. No respetar esto convierte cada app en una isla con codigo duplicado y bugs unicos.
### Flujo obligatorio antes de escribir codigo en un artefacto
1. **Consultar registry.db con FTS5** para encontrar funciones existentes que cubran el caso. No es opcional. Buscar por `name`, `description`, `tags`, `signature`, `code` y `params_schema`. Probar varios sinonimos (`http`, `serve`, `router`; `id`, `uuid`, `random_hex`; etc.).
2. **Reutilizar lo que existe**. Importar la funcion del registry y declararla en `uses_functions` del `app.md`. NO reescribir logica inline cuando ya hay una funcion.
3. **Si falta una pieza reutilizable → delegar a `fn-constructor`** (subagent_type `fn-constructor`). NO escribir la funcion inline en el artefacto. El agente construye la funcion en su sitio (`functions/{domain}/`, `python/functions/{domain}/`, etc.) con `.go/.py/.sh/.ts` + `.md` correctos, tests, y respetando las reglas de pureza/firma.
4. **Solo despues** se escribe el codigo del artefacto, que orquesta funciones del registry y aporta unicamente la logica especifica del dominio (CRUD de tablas concretas, layout de UI, flujo de la app).
### Que va al registry vs que va al artefacto
| Tipo de codigo | Donde |
|---|---|
| Logica reutilizable, primitiva, generica (parser, helper http, abrir SQLite, generar IDs, formatear timestamps con politica fija, middleware, etc.) | `functions/{domain}/` — delegar a `fn-constructor` si no existe |
| Composicion de varias funciones del registry para un flujo concreto | `pipelines` (registry) o codigo del artefacto segun reusabilidad |
| Schema SQL especifico del artefacto | Migraciones del artefacto |
| Handlers HTTP que solo hacen sentido para este artefacto (ej. `/api/board` de un kanban) | Codigo del artefacto, pero usando `http_json_response_go_infra`, `http_parse_body_go_infra`, etc. del registry |
| Layout/components especificos de la UI del artefacto | Codigo del artefacto, pero consumiendo componentes de `frontend/functions/ui/` (`@fn_library`) |
Regla practica: **si dos artefactos ya hacen o haran lo mismo, es funcion del registry**. One-liners idiomaticos de la stdlib (`time.Now().UTC().Format(...)`) NO necesitan ser registry — se ven en cualquier sitio. Pero un patron como "abrir SQLite con WAL + foreign keys + ping" SI (y por eso existe `sqlite_open_go_infra`).
### Cuando delegar a `fn-constructor`
Delegar SIEMPRE que se necesite una funcion reutilizable que no existe. El prompt del subagente debe incluir:
- Lenguaje, dominio, nombre propuesto.
- Firma esperada (params + return).
- Pureza (`pure` o `impure`).
- Una breve descripcion del proposito y del comportamiento.
- Si hay funciones similares en el registry, listarlas para evitar duplicados.
El agente construye la funcion siguiendo las reglas del registry (`purity.md`, `ids_naming.md`, `types_in_signatures.md`, etc.) y deja `fn index` listo para ejecutar.
### Auditoria
Despues de implementar el artefacto, verificar que `uses_functions` del `app.md` (o equivalente) declara TODAS las funciones del registry consumidas. Esto se puede cruzar con los `import` reales del codigo:
```bash
# Para Go:
grep -rh '"fn-registry/functions/' apps/<app>/ | sort -u
# Cada paquete importado tiene que tener al menos una funcion declarada en uses_functions.
```
### Por que esta regla
Sin esta regla cada app reinventa: helpers SQLite, middleware HTTP, generacion de IDs, parsers, validadores, formateo de fechas. El registry pierde su razon de ser. Con esta regla, una funcion bien hecha se reutiliza en N apps; un bug se arregla una vez; la velocidad de cada app nueva crece a medida que el registry crece.
+60
View File
@@ -0,0 +1,60 @@
## Extraccion de funciones desde repos externos (`sources/`)
### Workflow
1. Clonar repo en `sources/<nombre>` (gitignored, solo el manifest `sources/sources.yaml` se versiona)
2. El agente analiza el repo y propone funciones candidatas
3. Las funciones se **copian y adaptan** al formato del registry (.go/.py/.sh/.ts + .md con frontmatter)
4. `fn index` las registra. El manifest se actualiza con las funciones extraidas.
### Filtro de calidad (obligatorio antes de extraer)
Una funcion externa solo se extrae si cumple TODOS estos criterios:
- **Firma generica**: no depende de tipos internos del repo origen ni de config hardcodeada
- **Sin estado global**: no usa variables globales, singletons, ni init() con side effects
- **Dependencias minimas**: solo stdlib o dependencias ya presentes en fn_registry
- **Sin credenciales**: no contiene secrets, API keys, ni paths absolutos
- **Testeable**: la logica debe poder validarse con tests unitarios
- **No duplicada**: consultar registry.db con FTS5 antes de extraer para evitar duplicados
- **Licencia compatible**: el repo debe tener licencia permisiva (MIT, Apache 2.0, BSD, etc.)
### Clasificacion de pureza al extraer
Extraer tanto funciones puras como impuras. La clasificacion correcta es obligatoria:
- **Pure**: sin I/O, sin estado mutable, determinista. Extraer como `purity: pure`.
- **Impure**: hace I/O (red, disco, DB, HTTP), usa concurrencia, o depende de estado externo. Extraer como `purity: impure` con `error_type` apropiado.
- **Pipeline**: compone multiples funciones para un flujo completo. Extraer como `kind: pipeline`, siempre impuro.
No descartar funciones utiles solo por ser impuras. Una funcion que hace HTTP requests, lee archivos, o interactua con bases de datos es valiosa si su firma es generica y reutilizable.
### Adaptacion al extraer
- Renombrar a snake_case siguiendo la convencion del registry
- Adaptar firma para usar tipos nativos (no tipos internos del repo)
- Crear .md con frontmatter completo incluyendo `source_repo`, `source_license`, `source_file`
- Actualizar `sources/sources.yaml` con la extraccion
### Campos de atribucion en frontmatter
```yaml
source_repo: "https://github.com/user/project"
source_license: "MIT"
source_file: "pkg/original_file.go"
```
Estos campos se indexan en registry.db y permiten consultar:
```sql
SELECT id, source_repo, source_license FROM functions WHERE source_repo != '';
```
### Lenguajes soportados para extraccion
Cualquier lenguaje puede analizarse como fuente. El destino depende de la naturaleza de la funcion:
- Algoritmos/logica pura → Go (functions/{domain}/) o Python (python/functions/{domain}/)
- Funciones impuras (I/O, HTTP, DB) → Go o Python segun el dominio
- Scripts/utilidades sistema → Bash (bash/functions/{domain}/)
- UI/frontend → TypeScript (frontend/functions/{domain}/)
- Flujos multi-paso → Pipeline en el lenguaje mas natural
- C/Rust/otros → Traducir a Go o Python, manteniendo la semantica original
-5
View File
@@ -1,5 +0,0 @@
Los pipelines con tag `launcher` aparecen en el Pipeline Launcher TUI (`apps/pipeline_launcher`).
Sin el tag, el pipeline no es lanzable desde la TUI. Añadir `launcher` al array `tags` del .md al crear un pipeline ejecutable desde el launcher.
Pipelines interactivos (TUIs) o que no son subprocesos NO deben llevar este tag.
+35
View File
@@ -0,0 +1,35 @@
## uses_functions
Cuando un .cpp llama a otra funcion del registry, el `.md` del CONSUMIDOR
debe anadir la dependencia a `uses_functions`. El indexer NO lo deduce
automaticamente para C++ (parser no trivial).
Como auditar (funciones huerfanas):
sqlite3 registry.db "SELECT id FROM functions WHERE lang='cpp' AND uses_functions='[]';"
Como auditar (drift entre `CMakeLists.txt` y `app.md`):
- Cruzar los `${CMAKE_SOURCE_DIR}/functions/<dom>/<name>.cpp` listados en el
`CMakeLists.txt` con el `uses_functions` del `app.md`. Cada `.cpp` linkado
debe aparecer como `<name>_cpp_<dom>` en el `.md`. Excepciones: ver mas abajo.
Convencion:
- **Framework code** (`cpp/framework/app_base.cpp`) — no esta indexado.
- **Funciones bundled en `fn_framework`** — son funciones del registry cuyo
`.cpp` se compila dentro del static lib `fn_framework` (lista en
`cpp/CMakeLists.txt`, target `add_library(fn_framework STATIC ...)`):
`tokens`, `icon_font`, `app_settings`, `app_about`, `fps_overlay`,
`panel_menu`, `app_menubar`, `layouts_menu`, `logger`, `log_window`,
`gl_loader`, `layout_storage`, `selectable_text`. Las apps las usan
transitivamente (incluyen `core/logger.h`, llaman `fn_log::log_info`),
pero NO listan estos `.cpp` en su `CMakeLists.txt` (multiple-definition)
ni los declaran en `uses_functions` del `app.md`. Excepcion: si una app
toca una API que no este en fn_framework (raro), declara la dep.
- **TU adicional de un parent function** (ej. `graph_labels_select.cpp` que
va con `graph_labels.cpp`) — desde 2026-05-04 se registra como entrada
propia con su `.md` (ver ADR 0003). El parent declara la nueva entrada
en su `uses_functions`. Las apps que enlazan ambos `.cpp` listan ambas
IDs en `uses_functions` del `app.md`.
- **Apps** (`apps/`, `cpp/apps/`, `projects/*/apps/`) son leaves del grafo:
declaran `uses_functions` en `app.md` pero ninguna funcion del registry
las cita.
- DEMO_ONLY en `primitives_gallery` se etiqueta `notes: scaffolding/demo`.
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""
Extract Claude Design "standalone" HTML exports.
Claude Design packs the whole React app as base64+gzip blobs inside
<script type="__bundler/manifest"> tags. This script decompresses them
and writes each asset (JSX, CSS, fonts) to a target directory.
Usage:
python3 extract_design_bundle.py <path/to/export.html> <output_dir>
The output dir will contain:
data.jsx (if detected by header comment)
fn_library_emu.jsx (lib emulation)
charts_emu.jsx (charts emulation)
app.jsx (main tree)
<uuid>.<ext> (anything else — fonts, unknown js)
manifest.json (summary of all assets: uuid, mime, bytes, filename)
JSX files are named heuristically from their leading comment. If names
cannot be inferred from headers, they keep their uuid prefix.
"""
from __future__ import annotations
import base64
import gzip
import json
import pathlib
import re
import sys
MIME_TO_EXT = {
"text/javascript": "js",
"application/javascript": "js",
"text/babel": "jsx",
"application/json": "json",
"text/css": "css",
"image/svg+xml": "svg",
"font/woff2": "woff2",
"font/woff": "woff",
"text/html": "html",
}
# Order matters: first matching hint wins. Put MORE SPECIFIC patterns first.
HEADER_HINTS = [
("charts_emu.jsx", [r"Emulaci(ó|o)n de @fn_library/\{", r"LineChart, AreaChart, BarChart"]),
("fn_library_emu.jsx", [r"Emulaci(ó|o)n visual de @fn_library"]),
("data.jsx", [r"mock data \(determinista\)", r"window\.\w+Data\s*="]),
("app.jsx", [r"ReactDOM\.createRoot", r"arbol principal", r"function App\s*\("]),
]
def pick_name(content: str, used_names: set[str]) -> str | None:
head = content[:2000]
for name, patterns in HEADER_HINTS:
if name in used_names:
continue
if any(re.search(p, head, re.IGNORECASE) for p in patterns):
return name
return None
def grab_script(html: str, kind: str) -> str | None:
m = re.search(
r'<script type="__bundler/' + kind + r'">\s*(.*?)\s*</script>',
html, re.DOTALL,
)
return m.group(1) if m else None
def extract(html_path: pathlib.Path, out_dir: pathlib.Path) -> dict:
html = html_path.read_text(encoding="utf-8")
manifest_raw = grab_script(html, "manifest")
if not manifest_raw:
raise SystemExit(f"No <script type='__bundler/manifest'> found in {html_path}")
manifest = json.loads(manifest_raw)
ext_raw = grab_script(html, "ext_resources")
ext_resources = json.loads(ext_raw) if ext_raw else []
id_map = {e["uuid"]: e.get("id", e["uuid"]) for e in ext_resources}
out_dir.mkdir(parents=True, exist_ok=True)
summary = []
used_names: set[str] = set()
# First pass: decode all assets and collect jsx blobs (so we can name them by header hint)
decoded: list[tuple[str, str, bytes]] = [] # (uuid, mime, bytes)
for uuid, entry in manifest.items():
raw = base64.b64decode(entry["data"])
if entry.get("compressed"):
raw = gzip.decompress(raw)
decoded.append((uuid, entry.get("mime", "application/octet-stream"), raw))
# Second pass: write files with heuristic names for known jsx
for uuid, mime, raw in decoded:
ext = MIME_TO_EXT.get(mime, "bin")
filename = None
# Heuristic for JSX / JS that represents the app
if ext in ("jsx", "js"):
try:
text = raw.decode("utf-8", errors="replace")
name = pick_name(text, used_names)
if name:
filename = name
used_names.add(name)
except Exception:
pass
if not filename:
# Fall back to ext_resources id if present, or uuid
base = id_map.get(uuid, uuid)
safe = re.sub(r"[^A-Za-z0-9._-]", "_", base)[:80]
filename = f"{safe}.{ext}"
path = out_dir / filename
# Avoid collisions
i = 2
while path.exists():
stem = path.stem
path = out_dir / f"{stem}_{i}.{ext}"
i += 1
path.write_bytes(raw)
summary.append({
"uuid": uuid,
"mime": mime,
"bytes": len(raw),
"filename": path.name,
})
(out_dir / "manifest.json").write_text(
json.dumps({"source": str(html_path), "assets": summary}, indent=2),
encoding="utf-8",
)
return {"assets": summary, "out": str(out_dir)}
def main():
if len(sys.argv) < 3:
print(__doc__)
sys.exit(2)
html = pathlib.Path(sys.argv[1])
out = pathlib.Path(sys.argv[2])
if not html.exists():
sys.exit(f"Input not found: {html}")
result = extract(html, out)
print(f"✓ Extracted {len(result['assets'])} assets to {result['out']}")
print(f" Manifest: {out}/manifest.json")
print()
# Short preview per asset
for a in result["assets"]:
print(f" {a['mime']:28s} {a['bytes']:>8} B {a['filename']}")
if __name__ == "__main__":
main()
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env bash
# PostToolUse hook: registra cada invocacion del agente en
# projects/fn_monitoring/apps/call_monitor/operations.db (issue 0085b).
#
# Identifica tool, extrae function_id cuando es posible, clasifica el patron
# (mcp_*, fn_cli_run, heredoc_py, sqlite_direct, edit_registry, ...) y
# detecta antipatrones para registrar violations.
#
# NUNCA bloquea la herramienta. Falla silenciosamente si la BD no esta lista.
# Solo guarda args_hash, jamas valores concretos.
set -euo pipefail
# ---- Resolve registry root (walks up from cwd looking for registry.db) ----
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
DB="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
# Si la BD aun no existe, el hook no hace nada (esperando init).
[ -f "$DB" ] || exit 0
# ---- Read stdin JSON ----
INPUT=$(cat)
if [ -z "$INPUT" ]; then exit 0; fi
# Required jq presence
command -v jq >/dev/null 2>&1 || exit 0
command -v sqlite3 >/dev/null 2>&1 || exit 0
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""')
TS=$(date -u +%s)
# Tool response success/error
SUCCESS=1
ERROR_CLASS=""
ERROR_SNIPPET=""
RESP_IS_ERROR=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.is_error // false) else false end')
if [ "$RESP_IS_ERROR" = "true" ]; then
SUCCESS=0
ERROR_SNIPPET=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.error // .tool_response.content // "") else "" end' | head -c 240 | tr '\n' ' ')
fi
# args_hash: sha256 truncado del tool_input (sin valores)
ARGS_HASH=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' | sha256sum | cut -c1-16)
# Helpers SQL
sql_escape() { printf '%s' "$1" | sed "s/'/''/g"; }
insert_call() {
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}" snippet="${4:-}"
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc snip_esc
# Politica issue 0087: command_snippet solo se rellena cuando function_id
# esta vacio. Si la call golpea una funcion del registry, su ID y
# tool_used bastan; no duplicamos el comando.
if [ -n "$fn_id" ]; then snippet=""; fi
# Redact common secrets antes de persistir
snippet=$(printf '%s' "$snippet" \
| sed -E 's/(password|token|secret|api[_-]?key|bearer)([[:space:]]*[=:][[:space:]]*)[^[:space:]]+/\1\2<REDACTED>/Ig' \
| head -c 200)
fn_esc=$(sql_escape "$fn_id")
tu_esc=$(sql_escape "$tool_used")
ec_esc=$(sql_escape "$ERROR_CLASS")
es_esc=$(sql_escape "$ERROR_SNIPPET")
sid_esc=$(sql_escape "$SESSION_ID")
ah_esc=$(sql_escape "$ARGS_HASH")
snip_esc=$(sql_escape "$snippet")
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, command_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc','$snip_esc',$TS);" 2>/dev/null || true
}
insert_code_write() {
local fn_id="$1" file_path="$2" added="${3:-0}" removed="${4:-0}"
local fn_esc fp_esc sid_esc
fn_esc=$(sql_escape "$fn_id")
fp_esc=$(sql_escape "$file_path")
sid_esc=$(sql_escape "$SESSION_ID")
sqlite3 "$DB" "INSERT INTO code_writes (session_id, function_id, file_path, lines_added, lines_removed, ts) VALUES ('$sid_esc','$fn_esc','$fp_esc',$added,$removed,$TS);" 2>/dev/null || true
}
# Snapshot a function version row when an edit lands on a registry file.
# Uses sha256 of file bytes as content_hash (separate namespace from index source).
insert_edit_version() {
local fn_id="$1" abs_path="$2"
[ -f "$abs_path" ] || return 0
command -v sha256sum >/dev/null 2>&1 || return 0
local hash
hash=$(sha256sum "$abs_path" 2>/dev/null | awk '{print $1}')
[ -z "$hash" ] && return 0
local fn_esc h_esc
fn_esc=$(sql_escape "$fn_id")
h_esc=$(sql_escape "$hash")
sqlite3 "$DB" "INSERT OR IGNORE INTO function_versions (function_id, content_hash, version, snapped_at, source, lines_added, lines_removed) VALUES ('$fn_esc','$h_esc','',$TS,'edit_hook',0,0);" 2>/dev/null || true
}
insert_violation() {
local rule_id="$1" fn_id="$2" snippet="$3" severity="${4:-warning}"
local r_esc fn_esc sn_esc sev_esc sid_esc
r_esc=$(sql_escape "$rule_id")
fn_esc=$(sql_escape "$fn_id")
sn_esc=$(sql_escape "$(printf '%s' "$snippet" | head -c 240 | tr '\n' ' ')")
sev_esc=$(sql_escape "$severity")
sid_esc=$(sql_escape "$SESSION_ID")
sqlite3 "$DB" "INSERT INTO violations (session_id, rule_id, function_id, command_snippet, severity, ts) VALUES ('$sid_esc','$r_esc','$fn_esc','$sn_esc','$sev_esc',$TS);" 2>/dev/null || true
}
# ---- Derive function_id from registry file path ----
# Matches paths under functions/<domain>/<name>.<ext>, python/functions/<domain>/<name>.py,
# bash/functions/<domain>/<name>.sh, frontend/functions/<domain>/<name>.ts(x)
derive_fn_id_from_path() {
local p="$1"
[ -z "$p" ] && return 1
case "$p" in
functions/*/*.go|*/functions/*/*.go)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|.*functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|.*functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_go_%s' "$name" "$dom" && return 0 ;;
python/functions/*/*.py)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|python/functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|python/functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_py_%s' "$name" "$dom" && return 0 ;;
bash/functions/*/*.sh)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|bash/functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|bash/functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_bash_%s' "$name" "$dom" && return 0 ;;
frontend/functions/*/*.ts|frontend/functions/*/*.tsx)
local dom name
dom=$(printf '%s' "$p" | sed -E 's|frontend/functions/([^/]+)/.*|\1|')
name=$(printf '%s' "$p" | sed -E 's|frontend/functions/[^/]+/([^/.]+)\..*|\1|')
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_ts_%s' "$name" "$dom" && return 0 ;;
esac
return 1
}
# ---- Dispatch by tool ----
case "$TOOL_NAME" in
mcp__registry__fn_search)
insert_call "" "mcp_fn_search"
;;
mcp__registry__fn_show)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_show"
;;
mcp__registry__fn_code)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_code"
;;
mcp__registry__fn_uses)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_uses"
;;
mcp__registry__fn_run)
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
insert_call "$ID" "mcp_fn_run"
;;
mcp__registry__fn_list_domains)
insert_call "" "mcp_fn_list_domains"
;;
mcp__registry__fn_proposal)
insert_call "" "mcp_fn_proposal"
;;
mcp__registry__fn_doctor)
insert_call "" "mcp_fn_doctor"
;;
mcp__registry__fn_create_function)
insert_call "" "mcp_fn_create_function"
;;
Edit|Write|MultiEdit)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')
ABS_PATH="$FILE_PATH"
# Make path relative to root if absolute and inside root
case "$FILE_PATH" in
"$ROOT"/*) FILE_PATH="${FILE_PATH#$ROOT/}" ;;
/*) ABS_PATH="$FILE_PATH" ;;
*) ABS_PATH="$ROOT/$FILE_PATH" ;;
esac
FN_ID=$(derive_fn_id_from_path "$FILE_PATH" || true)
if [ -n "$FN_ID" ]; then
insert_code_write "$FN_ID" "$FILE_PATH" 0 0
insert_call "$FN_ID" "edit_registry"
insert_edit_version "$FN_ID" "$ABS_PATH"
fi
;;
Bash)
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""')
CMD_HEAD=$(printf '%s' "$CMD" | head -c 200 | tr '\n' ' ')
# Classify
TOOL_USED="bash_other"
FN_ID=""
if printf '%s' "$CMD" | grep -qE '(^|[[:space:]])\./fn[[:space:]]+run[[:space:]]+'; then
TOOL_USED="fn_cli_run"
FN_ID=$(printf '%s' "$CMD" | sed -nE 's/.*\.\/fn[[:space:]]+run[[:space:]]+([A-Za-z0-9_]+).*/\1/p' | head -n1)
elif printf '%s' "$CMD" | grep -qE 'python/\.venv/bin/python3[[:space:]]+-[[:space:]]+<<'; then
TOOL_USED="heredoc_py"
elif printf '%s' "$CMD" | grep -qE 'sqlite3[[:space:]][^|]*\bregistry\.db\b'; then
TOOL_USED="sqlite_direct"
fi
insert_call "$FN_ID" "$TOOL_USED" 0 "$CMD_HEAD"
# ---- Violation rules ----
# 1. sqlite3 directo SELECT sobre registry.db (excepto schema/pragma/count/join)
if [ "$TOOL_USED" = "sqlite_direct" ]; then
if ! printf '%s' "$CMD" | grep -qiE '(\.schema|\.tables|PRAGMA[[:space:]]+(table_info|index_list)|COUNT\(|GROUP[[:space:]]+BY|JOIN[[:space:]])'; then
insert_violation "sqlite3_registry_select" "" "$CMD_HEAD" "warning"
fi
fi
# 2. python -c "import X; dir(X)"
if printf '%s' "$CMD" | grep -qE 'python[3]?[[:space:]]+-c[[:space:]]+["'\''].*import.*(dir|help)\('; then
insert_violation "python_dir_inspect" "" "$CMD_HEAD" "info"
fi
# 3. from <pkg> import * (en heredoc python)
if [ "$TOOL_USED" = "heredoc_py" ]; then
if printf '%s' "$CMD" | grep -qE 'from[[:space:]]+[A-Za-z0-9_.]+[[:space:]]+import[[:space:]]+\*'; then
insert_violation "import_star_in_heredoc" "" "$CMD_HEAD" "warning"
fi
if printf '%s' "$CMD" | grep -qE 'client\._http\.request\('; then
insert_violation "client_http_request_direct" "" "$CMD_HEAD" "warning"
fi
fi
;;
esac
exit 0
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: inyecta capacidades calientes (TOP/FRESH/PIPELINES)
# del registry como additionalContext en cada turno del usuario.
#
# Cache: ~/.cache/fn_registry/capabilities.txt (TTL 1h).
# Fuente: `./fn doctor capabilities --emit-claude-md` desde la raiz del repo.
#
# NUNCA bloquea: si algo falla, emite contexto vacio y sale 0.
set -uo pipefail
CACHE_DIR="${HOME}/.cache/fn_registry"
CACHE_FILE="${CACHE_DIR}/capabilities.txt"
TTL_SECONDS=3600
# Resolve registry root (walks up from cwd, fallback CLAUDE_PROJECT_DIR)
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ] && [ -x "$d/fn" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "${CLAUDE_PROJECT_DIR}/registry.db" ]; then
printf '%s' "${CLAUDE_PROJECT_DIR}"
return 0
fi
return 1
}
# Consume stdin (UserPromptSubmit payload) — we don't need it but keep stdin clean
cat >/dev/null 2>&1 || true
ROOT=$(resolve_root) || exit 0
mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0
# Cache freshness check
need_refresh=1
if [ -f "$CACHE_FILE" ]; then
now=$(date +%s)
mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)
age=$((now - mtime))
if [ "$age" -lt "$TTL_SECONDS" ]; then
need_refresh=0
fi
fi
if [ "$need_refresh" -eq 1 ]; then
# Regenerate: call fn doctor capabilities --emit-claude-md and process
raw=$("$ROOT/fn" doctor capabilities --emit-claude-md 2>/dev/null || true)
if [ -z "$raw" ]; then
exit 0
fi
# Extract top 5 from each section using awk.
# Sections detected by "## ... Top" / "## ... Fresh" / "## ... Pipelines".
line=$(printf '%s\n' "$raw" | awk '
BEGIN { sec=""; n_top=0; n_fresh=0; n_pipe=0; }
/^## .*Top 20/ { sec="TOP"; next }
/^## .*Fresh/ { sec="FRESH"; next }
/^## .*Pipelines/ { sec="PIPE"; next }
/^## / { sec=""; next }
/^- `/ {
# extract first backticked token
s = $0
sub(/^- `/, "", s)
i = index(s, "`")
if (i == 0) next
id = substr(s, 1, i-1)
if (sec == "TOP" && n_top < 5) { tops[n_top++] = id }
if (sec == "FRESH" && n_fresh < 5) { fresh[n_fresh++] = id }
if (sec == "PIPE" && n_pipe < 5) { pipes[n_pipe++] = id }
}
END {
out = "CAPABILITIES (cache 1h):"
if (n_top > 0) {
line = " TOP: " tops[0]
for (i=1; i<n_top; i++) line = line ", " tops[i]
out = out "\n" line
}
if (n_fresh > 0) {
line = " FRESH (7d): " fresh[0]
for (i=1; i<n_fresh; i++) line = line ", " fresh[i]
out = out "\n" line
}
if (n_pipe > 0) {
line = " PIPELINES: " pipes[0]
for (i=1; i<n_pipe; i++) line = line ", " pipes[i]
out = out "\n" line
}
print out
}
')
if [ -z "$line" ]; then
exit 0
fi
printf '%s\n' "$line" >"$CACHE_FILE" 2>/dev/null || exit 0
fi
# Emit cached content as additionalContext
if [ ! -s "$CACHE_FILE" ]; then
exit 0
fi
ctx=$(cat "$CACHE_FILE")
if command -v jq >/dev/null 2>&1; then
jq -n --arg ctx "$ctx" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
else
# Fallback: print raw text (Claude Code prints stdout as context too)
printf '%s\n' "$ctx"
fi
exit 0
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# PostToolUse hook: gate "tag de capability group obligatorio" tras crear/modificar
# funciones del registry. Issue 0086 paso 9/gate.
#
# Comportamiento:
# - Detecta .md de funciones (functions/, python/functions/, bash/functions/,
# frontend/functions/, cpp/functions/) modificados en los ultimos 60s.
# - Lee frontmatter `tags:` y verifica si al menos uno coincide con un capability
# group declarado en docs/capabilities/INDEX.md.
# - Si NO hay match -> emite additionalContext con la lista de funciones afectadas.
# - NUNCA bloquea. Solo warning visible.
#
# Salida JSON consumida por Claude Code:
# { "hookSpecificOutput": { "hookEventName": "PostToolUse",
# "additionalContext": "..." } }
set -euo pipefail
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
INDEX="$ROOT/docs/capabilities/INDEX.md"
# Si no existe el INDEX aun, no hay grupos definidos -> nada que verificar.
[ -f "$INDEX" ] || exit 0
# Consume stdin (sin parsear — no necesitamos session_id para este gate)
cat >/dev/null
# Solo correr si hay jq disponible
command -v jq >/dev/null 2>&1 || exit 0
# 1. Cargar lista de capability groups desde el INDEX.
# Formato esperado en INDEX.md: | [name](name.md) | N | descripcion |
CAP_GROUPS=$(grep -oE '\[[a-z][a-z0-9_-]*\]\([a-z][a-z0-9_-]*\.md\)' "$INDEX" \
| sed -E 's/^\[([^]]+)\].*/\1/' \
| sort -u)
[ -z "$CAP_GROUPS" ] && exit 0
# 2. Encontrar .md de funciones modificados en ultimos 60s.
RECENT=$(find "$ROOT/functions" "$ROOT/python/functions" "$ROOT/bash/functions" \
"$ROOT/frontend/functions" "$ROOT/cpp/functions" \
-maxdepth 4 -type f -name '*.md' -mmin -1 2>/dev/null || true)
[ -z "$RECENT" ] && exit 0
# 3. Para cada .md reciente: extraer tags del frontmatter, comparar con groups.
MISSING=""
while IFS= read -r mdfile; do
[ -z "$mdfile" ] && continue
# Extrae el bloque entre los dos `---` del inicio
front=$(awk '/^---$/{c++; next} c==1 {print} c>=2 {exit}' "$mdfile" 2>/dev/null || true)
[ -z "$front" ] && continue
# tags: [a, b, c] o tags:\n - a\n - b
tags_inline=$( { printf '%s\n' "$front" | grep -E '^tags:[[:space:]]*\[' | head -1 \
| sed -E 's/^tags:[[:space:]]*\[(.*)\].*$/\1/' \
| tr ',' '\n' | sed -E 's/^[[:space:]"]+|[[:space:]"]+$//g'; } || true )
tags_block=$( { printf '%s\n' "$front" | awk '
/^tags:[[:space:]]*$/ {intag=1; next}
intag && /^[[:space:]]*-[[:space:]]/ {sub(/^[[:space:]]*-[[:space:]]*/, ""); print; next}
intag && !/^[[:space:]]/ {intag=0}
' | sed -E 's/^[[:space:]"]+|[[:space:]"]+$//g'; } || true )
tags=$( { printf '%s\n%s\n' "$tags_inline" "$tags_block" | grep -v '^$'; } || true )
matched=0
while IFS= read -r g; do
[ -z "$g" ] && continue
if printf '%s\n' "$tags" | grep -qx "$g"; then
matched=1
break
fi
done <<< "$CAP_GROUPS"
if [ "$matched" -eq 0 ]; then
rel="${mdfile#$ROOT/}"
MISSING="${MISSING}${rel}\n"
fi
done <<< "$RECENT"
# 4. Si hay funciones sin tag de grupo, emitir aviso.
if [ -n "$MISSING" ]; then
CAP_GROUPS_CSV=$(printf '%s' "$CAP_GROUPS" | tr '\n' ',' | sed 's/,$//')
WARN="CAPABILITY-GAP (issue 0086): funcion(es) recien tocada(s) sin tag de capability group: $(printf '%b' "$MISSING" | tr '\n' ' ')"
WARN+="| Grupos disponibles: ${CAP_GROUPS_CSV}. Anade al menos uno al frontmatter \`tags:\` y corre \`./fn index\`. Si la funcion no encaja en ningun grupo existente, considera crear grupo nuevo (>=3 funciones) o dejarla con tag plano (no de grupo)."
jq -n --arg ctx "$WARN" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}'
fi
exit 0
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# PreToolUse hook: sugiere funciones del registry cuando un comando Bash
# inline probablemente reinventa una funcion existente (issue 0087).
#
# Llama a `./fn match "<cmd>"` con timeout 200ms. Si encaja con alta
# confianza, imprime un <system-reminder> a stderr para que Claude Code
# lo lea como recordatorio. NUNCA bloquea la tool — exit 0 siempre.
set -euo pipefail
# ---- Always exit 0, no matter what ----
trap 'exit 0' ERR
# ---- Resolve registry root (walks up from cwd) ----
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
FN_BIN="$ROOT/fn"
[ -x "$FN_BIN" ] || exit 0
# ---- Read stdin JSON ----
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
[ -z "$INPUT" ] && exit 0
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
[ "$TOOL_NAME" = "Bash" ] || exit 0
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
[ -z "$CMD" ] && exit 0
# Single-line for matching against denylist patterns
CMD_FLAT=$(printf '%s' "$CMD" | tr '\n' ' ')
# ---- Denylist (skip antes de llamar fn match para ahorrar el invoke) ----
# Comandos demasiado cortos -> trivial
CMD_LEN=${#CMD_FLAT}
[ "$CMD_LEN" -lt 20 ] && exit 0
# Trivial single-utility commands
case "$CMD_FLAT" in
"ls"|"ls "*|"cd"|"cd "*|"pwd"|"pwd "*|"cat"|"cat "*|"echo"|"echo "*)
exit 0 ;;
"grep"|"grep "*|"head"|"head "*|"tail"|"tail "*|"wc"|"wc "*)
exit 0 ;;
"mkdir"|"mkdir "*|"rm"|"rm "*|"mv"|"mv "*|"cp"|"cp "*)
exit 0 ;;
"git"|"git "*)
exit 0 ;;
"go"|"go "*)
# go build / go test corrientes — el agente ya los maneja
exit 0 ;;
esac
# Comandos que ya usan el registry: ./fn ..., fn run ..., mcp__registry__*
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])\./fn([[:space:]]|$)'; then
exit 0
fi
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])fn[[:space:]]+(run|search|show|code|uses|doctor|index|match|list|add|proposal|sync|ops|check)'; then
exit 0
fi
# Pure-cd (movement only, no logic)
if printf '%s' "$CMD_FLAT" | grep -qE '^[[:space:]]*cd[[:space:]]+[^&|;]+$'; then
exit 0
fi
# ---- Llamar fn match con timeout 200ms ----
command -v timeout >/dev/null 2>&1 || exit 0
# Truncar el comando a algo razonable para fn match (evitar args huge)
CMD_TRUNC=$(printf '%s' "$CMD_FLAT" | head -c 500)
MATCH_JSON=$(timeout 0.2 "$FN_BIN" match "$CMD_TRUNC" --format json --top 3 2>/dev/null) || exit 0
[ -z "$MATCH_JSON" ] && exit 0
# ---- Parsear JSON ----
HIGH_CONF=$(printf '%s' "$MATCH_JSON" | jq -r '.high_confidence // false' 2>/dev/null || echo "false")
TOP_ID=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].id // ""' 2>/dev/null || echo "")
TOP_SCORE=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].score // 0' 2>/dev/null || echo "0")
TOP_SIG=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].signature // ""' 2>/dev/null || echo "")
TOP_SNIP=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].snippet // ""' 2>/dev/null || echo "")
[ -z "$TOP_ID" ] && exit 0
# Trigger condition: (high_confidence==true OR score>=0.85) AND score>=0.6
# - high_confidence requires top1/top2 gap > 1.5 (set por fn match)
# - score>=0.85 cubre matches muy fuertes donde el gap es modesto
SCORE_HI=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.85) ? "1" : "0" }')
SCORE_MIN=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.6) ? "1" : "0" }')
[ "$SCORE_MIN" = "1" ] || exit 0
if [ "$HIGH_CONF" != "true" ] && [ "$SCORE_HI" != "1" ]; then
exit 0
fi
# Truncar snippet a 100 chars y limpiar saltos de linea
SNIP_SHORT=$(printf '%s' "$TOP_SNIP" | tr '\n' ' ' | head -c 100)
# Formatear score con 2 decimales
SCORE_FMT=$(awk -v s="$TOP_SCORE" 'BEGIN{ printf "%.2f", s+0 }')
# ---- Emitir <system-reminder> a stderr ----
cat >&2 <<EOF
<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function.
USE: ./fn run $TOP_ID -> $TOP_SIG
SNIPPET: $SNIP_SHORT
Confidence: $SCORE_FMT. If you proceed inline, the violation will be logged.
</system-reminder>
EOF
exit 0
# Test manual:
# echo '{"tool_name":"Bash","tool_input":{"command":"taskkill.exe /IM registry_dashboard.exe /F"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
#
# Casos silenciosos:
# echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
# echo '{"tool_name":"Bash","tool_input":{"command":"./fn run filter_slice_go_core 1 2 3"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: recordatorio compacto de patrones canonicos del registry.
# Inyectado como additionalContext en cada turno del usuario.
# Issue 0085 (hardening 2).
#
# NUNCA bloquea. Solo printf de additionalContext.
set -euo pipefail
# Resolve registry root (walks up from cwd)
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
# Read input, extract session_id (UserPromptSubmit payload includes it)
INPUT=$(cat)
SESSION_ID=""
if command -v jq >/dev/null 2>&1; then
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || true)
fi
# Count current pending proposals + recent violations for situational awareness
PROPOSALS_PENDING="?"
VIOLATIONS_24H="?"
CALLS_24H="?"
CAP_CREATED=0
CAP_USED=0
CAP_ORPHAN=0
if command -v sqlite3 >/dev/null 2>&1; then
REG="$ROOT/registry.db"
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
[ -f "$REG" ] && PROPOSALS_PENDING=$(sqlite3 "$REG" "SELECT COUNT(*) FROM proposals WHERE status='pending'" 2>/dev/null || echo "?")
if [ -f "$MON" ]; then
VIOLATIONS_24H=$(sqlite3 "$MON" "SELECT COUNT(*) FROM violations WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)" 2>/dev/null || echo "?")
CALLS_24H=$(sqlite3 "$MON" "SELECT COUNT(*) FROM calls WHERE ts >= CAST(strftime('%s','now','-1 day') AS INTEGER)" 2>/dev/null || echo "?")
if [ -n "$SESSION_ID" ]; then
sid_esc=$(printf '%s' "$SESSION_ID" | sed "s/'/''/g")
CAP_CREATED=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc'" 2>/dev/null || echo 0)
CAP_USED=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc' AND calls_in_session>0" 2>/dev/null || echo 0)
CAP_ORPHAN=$(sqlite3 "$MON" "SELECT COUNT(*) FROM session_capability_growth WHERE session_id='$sid_esc' AND calls_in_session=0" 2>/dev/null || echo 0)
fi
fi
fi
REMINDER="REGISTRY-FIRST (issue 0085 telemetry active): "
REMINDER+="Inspect → mcp__registry__fn_search/show/code/uses/proposal. "
REMINDER+="Execute one fn → mcp__registry__fn_run or ./fn run. "
REMINDER+="Compose multi-fn → heredoc python IMPORTANDO del registry. "
REMINDER+="NUNCA sqlite3 registry.db directo (salvo schema/PRAGMA/COUNT/JOIN). "
REMINDER+="NUNCA reescribir inline logica que ya es funcion. "
REMINDER+="Si patron se repite >2x → propose nueva funcion via fn-constructor. "
REMINDER+="Estado: pending_proposals=${PROPOSALS_PENDING} violations_24h=${VIOLATIONS_24H} calls_24h=${CALLS_24H}. "
REMINDER+="CAPABILITY-GROWTH (issue 0086): created_this_session=${CAP_CREATED} used=${CAP_USED} orphan=${CAP_ORPHAN}. Si orphan>0 -> integra la funcion en el codigo o documenta por que se quedo huerfana. "
REMINDER+="Comando autocheck: /fn_claude."
jq -n --arg ctx "$REMINDER" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# PreToolUse hook: NO bloquea. Inyecta recordatorio cuando ve sqlite3 sobre registry.db
# para que el modelo prefiera el MCP `registry` la proxima vez.
input="$(cat)"
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // ""')"
# Solo nos importa registry.db (NO operations.db, NO otros .db).
if ! printf '%s' "$cmd" | grep -Eq 'sqlite3[^|]*\bregistry\.db\b'; then
exit 0
fi
# Casos legitimos donde el MCP no aplica: introspeccion de schema, agregaciones, JOINs.
if printf '%s' "$cmd" | grep -Eq '(\.schema|\.tables|PRAGMA[[:space:]]+(table_info|index_list))'; then
exit 0
fi
if printf '%s' "$cmd" | grep -Eqi '(COUNT\(|GROUP[[:space:]]+BY|JOIN[[:space:]])'; then
exit 0
fi
# Caso a redirigir: emitir nota como additionalContext y dejar pasar el comando.
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: "Aviso: sqlite3 directo sobre registry.db detectado. Para futuras consultas usa el MCP registry (mcp__registry__fn_search / fn_show / fn_code / fn_uses / fn_list_domains). Fallback a sqlite3 SOLO para .schema, PRAGMA, COUNT/GROUP BY, JOINs custom."
}
}'
exit 0
+55 -1
View File
@@ -1,7 +1,8 @@
# SQLite index (regenerable con fn index) — SOLO en raiz
# SQLite index regenerable con `fn index` + completable con `fn sync`
registry.db
registry.db-journal
registry.db-wal
registry.db-shm
# operations.db — datos vivos, cada app genera el suyo con fn ops init
**/operations.db
@@ -24,6 +25,59 @@ registry.db-wal
*.swo
*~
# Secrets
**/.env
**/.env.*
# Python
**/__pycache__/
**/*.pyc
**/*.pyo
python/.venv/
# Externalized apps and analysis (each is its own Gitea repo)
apps/*/
analysis/*/
# Projects (each is its own git repo, only project.md templates are versioned)
projects/*/
# Vaults — data stores (symlinks, dirs, files); only vault.yaml manifest is versioned
vaults/*/
!vaults/vault.yaml
# Node / pnpm
**/node_modules/
# Sources — repos externos clonados (solo se versiona el manifest)
sources/*/
# Subrepos — mirrors/espejos externos (cada uno su propio git remote)
subrepos/*/
# External — symlinks a repos ajenos (ej: repo_Claude con skills/commands)
external/
# Worktrees — git worktrees para issues paralelos (parallel-fix-issues)
worktrees/
# Claude runtime locks
.claude/scheduled_tasks.lock
# Temp — workspace efimero para pruebas rapidas (APIs, scripts, analisis)
temp/
# C++ build artifacts
cpp/build/
# OS
.DS_Store
Thumbs.db
# Archivos locales
.local
broken_paths.txt
imgui.ini
prompts/
kotlin/functions/ui/
+22
View File
@@ -0,0 +1,22 @@
[submodule "cpp/vendor/imgui"]
path = cpp/vendor/imgui
url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "cpp/vendor/implot"]
path = cpp/vendor/implot
url = https://github.com/epezent/implot.git
[submodule "cpp/vendor/tracy"]
path = cpp/vendor/tracy
url = https://github.com/wolfpld/tracy.git
[submodule "cpp/vendor/glfw"]
path = cpp/vendor/glfw
url = https://github.com/glfw/glfw.git
[submodule "cpp/vendor/implot3d"]
path = cpp/vendor/implot3d
url = https://github.com/brenocq/implot3d.git
[submodule "cpp/vendor/sdl3"]
path = cpp/vendor/sdl3
url = https://github.com/libsdl-org/SDL.git
[submodule "emsdk"]
path = emsdk
url = https://github.com/emscripten-core/emsdk.git
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"registry": {
"command": "./apps/registry_mcp/registry_mcp",
"args": ["--enable-run", "--enable-write"]
}
}
}
+272
View File
@@ -0,0 +1,272 @@
# Changelog
Todos los cambios notables de `fn_registry` se documentan aquí.
Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/). Al no haber releases semver formales, las entradas se ordenan por fecha.
Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones arquitecturales ver `docs/adr/`.
## [Unreleased]
## 2026-05-14
### Added
- **Issue 0086 — Monitor tab del `registry_dashboard`** (sub-repo `dataforge/registry_dashboard`). Pestaña `Monitor` primera y por defecto del TabBar, landing del bucle reactivo construir->ejecutar->recopilar->analizar->mejorar.
- 7 KPIs (Calls / MCP / Reg % / Errors / Violations / Copies / Versions) filtradas por ventana temporal (1h/24h/7d/30d/All).
- Sub-tab `Recent Executions` con columnas When/Function/Tool/ms/OK/Error. Columna Function muestra `$ <snippet>` en gris cuando `function_id` vacio, hover tooltip con comando completo. Checkbox `Only registry functions` filtra por `function_id != ''`.
- Sub-tab `Failed Functions` (5a) — subset filtrado a registry-functions fallidas, columnas When/Function/Tool/Error class/Error snippet, function_id en rojo.
- Live scatter `duracion (ms)` vs `time`: eje X auto-scroll a `now`, ventana configurable (1m/5m/15m/1h/6h) independiente del filtro de KPIs, eje Y dinamico `0..max(visible)+500ms`. Hora local (`UseLocalTime`). Series ok/error en verde/rojo. Hover sobre punto = tooltip Function/Tool/Duration/Error.
- Indicador `live`/`offline` con timestamp del ultimo evento WS.
- **WebSocket live stream sqlite_api -> registry_dashboard** (sub-repo `dataforge/sqlite_api`). Endpoint `GET /api/events/call_monitor`. Hub global con subscribers; ticker arranca solo con >=1 subscriber (cero overhead si nadie mira). Cliente recibe snapshot inicial (KPIs + 100 ultimas filas + watermark) y luego deltas `id > watermark`. Cliente puede mandar `{watermark: N}` para resumir tras reconexion.
- **WS client C++** hand-rolled RFC6455 en `ws_client.{h,cpp}` (~330 LOC) en el dashboard. Localhost-only (no TLS). Thread propio, reconnect exponencial 0.5s->8s, FIN/text/ping/pong/close handling, queue thread-safe drenada cada frame.
- **Migration 007 `command_snippet` en `calls`** (`projects/fn_monitoring/apps/call_monitor/migrations/007_calls_command_snippet.sql`). Aditiva, idempotente. Llena por hook `hook_call_monitor.sh` solo cuando `function_id == ''`. Redactado de `password=`/`token=`/`secret=`/`api_key=`/`bearer=`. Truncado 200 chars.
- **Issue 0087 — Capability Discovery Acceleration**. Modelo 5 capas + 7 piezas (ver `dev/issues/0087-*.md`).
- **`fn match`** (`cmd/fn/match.go`) — subcommand fuzzy-FTS5 que dado un comando devuelve top-N funciones del registry candidates. Latencia 6-7ms. Output JSON con `score` (normalizado top=1.0) + `raw_score` (absoluto pre-normalizacion) + `high_confidence` gate (`raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`).
- **`fn doctor capabilities --emit-claude-md`** (`cmd/fn/doctor.go` + `functions/infra/emit_capabilities_md.go`) — emite bloque markdown con secciones TOP 20 (por `calls_total`), Fresh 7d, Pipelines top 5. Fallback si `call_monitor.operations.db` ausente.
- **`call_monitor sequences --detect [--propose]`** (`projects/fn_monitoring/apps/call_monitor/sequences.go` + `migrations/006_function_sequences.sql`). Detecta secuencias A->B(->C) en `calls` (same session, gap < 30s, occ >= 5, sess >= 2, success_rate >= 0.9) y abre proposals `new_pipeline` automaticamente.
- **Hook `PreToolUse` `hook_fn_match.sh`** — denylist + `fn match` con timeout 0.2s. Inyecta `<system-reminder>FUZZY-MATCH: USE ./fn run <id>` cuando confidence alta. Latencia 113ms trigger / 32ms denylist. Registrado en `.claude/settings.local.json` (Bash matcher).
- **Hook `UserPromptSubmit` `hook_capabilities_inject.sh`** — cache 1h en `~/.cache/fn_registry/capabilities.txt`. Emite JSON `hookSpecificOutput.additionalContext` con linea compacta `CAPABILITIES: TOP / FRESH / PIPELINES`. Latencia cold 33ms / warm 18ms.
- **Timer systemd user** `call_monitor_sequences.timer` (OnCalendar 0/6h) + `.service` oneshot ejecutando `call_monitor sequences --detect --propose --report`. Versionado en `projects/fn_monitoring/apps/call_monitor/systemd/`.
- **3 funciones nuevas grupo `cpp-windows`** + pagina madre `docs/capabilities/cpp-windows.md`:
- `launch_cpp_app_windows_bash_infra``cmd.exe`/`PowerShell Start-Process` para lanzar exe en Windows desde WSL2.
- `is_cpp_app_running_windows_bash_infra``tasklist.exe /FI` con exit code 0/1 + stdout `RUNNING: PID=N MEM=K` o `NOT_RUNNING`.
- `redeploy_cpp_app_windows_bash_pipelines` — pipeline build? + deploy + launch + verify en 1 invocacion. Reemplaza ~6 commands manuales.
- **ADR 0004 `docs/adr/0004-telemetry-driven-capability-growth.md`** — formaliza el bucle telemetria -> proposal -> capability group -> discovery acceleration como motor de crecimiento del registry.
- **Regla `.claude/rules/function_growth_and_self_docs.md`** (entry #30 en `INDEX.md`) — contrato `.md` autosuficiente (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por promocion de composiciones, NO por inflado de funciones individuales.
### Changed
- **`.claude/CLAUDE.md` Norte ampliado** — 4o objetivo `PROMOVER COMPOSICIONES A PIPELINES` (el registry crece por composicion, no por inflado). Linea sobre auto-discovery zero-second-lookup.
- **`.claude/rules/registry_calls.md`** — clausula nueva: hooks e infraestructura de telemetria (`fn_match`, `fn doctor`, `call_monitor`) pueden leer `registry.db` directo con conexion read-only. NO sujeto a regla MCP-first (no son acciones del agente).
- **`/fn_claude` command** mejorado con objetivos del Monitor + interpretacion de `FUZZY-MATCH` hint + `CAPABILITIES` line + threshold semantica.
### Fixed
- **`launch_cpp_app_windows` quoting bug** — `cmd.exe /c "cd /d \"$dir\" && start ..."` rompia con paths Windows (el `\"` final se interpretaba como escape de comilla -> string sin cerrar -> "Windows cannot find \\"). Fix: reescribir a `powershell.exe -Command "Start-Process -FilePath ... -WorkingDirectory ..."` (single-quote PowerShell es literal, sin procesar `\` ni `$`).
- **`fn match high_confidence` siempre true** — debido a normalizacion `top=1.0`. Fix: añadir `raw_score` preservado pre-normalizacion + gate dual `raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`. Threshold 4.0 tuneado contra 14 patrones del analysis `domain_coverage_gaps` (~93% precision).
## 2026-05-07
### Added
- **`fn doctor` CLI** (`cmd/fn/doctor.go`) — entrypoint unico read-only para diagnostico del registry y artefactos. Subcomandos: `artefacts` (git/venv/app.md/upstream), `services` (apps tag service + systemctl + puerto), `sync` (drift `pc_locations` BD vs disco), `uses-functions` (imports reales vs declarados en `app.md`), `unused` (funciones sin consumidores). Flag `--json` para agentes/scripts. Cada subcomando es wrapper fino sobre una funcion del registry.
- `.claude/rules/fn_doctor.md` — regla 23 en `INDEX.md`. Documenta cuando usar, mapeo subcomando → funcion del registry, y acciones derivadas (que hacer cuando reporta un drift).
- `bash/functions/infra/backup_sqlite_db` (`backup_sqlite_db_bash_infra`, **impure**) — snapshot atomico de SQLite via `VACUUM INTO`. Mas seguro que `cp` con escrituras concurrentes.
- `bash/functions/infra/rotate_backups` (`rotate_backups_bash_infra`, **impure**) — retention rsnapshot-style `daily.N/weekly.M/monthly.K`.
- `bash/functions/infra/wait_for_http` (`wait_for_http_bash_infra`, **impure**) — poll URL hasta 2xx con timeout, util en deploys/smoke tests.
- `bash/functions/infra/wait_for_port` (`wait_for_port_bash_infra`, **impure**) — poll TCP host:puerto. Usa `nc` o `/dev/tcp` builtin (sin deps).
- `bash/functions/infra/port_kill` (`port_kill_bash_infra`, **impure**) — mata proceso(s) escuchando un puerto. Idempotente, fallback `KILL` tras `TERM`.
- `bash/functions/infra/tail_journal` (`tail_journal_bash_infra`, **impure**) — wrapper `journalctl` con auto-deteccion `--user` vs sistema, prioridad y `--since`.
- `bash/functions/infra/pre_commit_hook_install` (`pre_commit_hook_install_bash_infra`, **impure**) — instala hook que llama `scan_secrets_in_dirty_bash_cybersecurity` antes de cada commit. Idempotente con marca `fn_registry-pre-commit-v1`.
- `functions/infra/notify_telegram` (`notify_telegram_go_infra`, **impure**) — envia mensaje a chat Telegram via Bot API. Trunca >4096 chars.
- `functions/infra/artefact_doctor` (`artefact_doctor_go_infra`, **impure**) — audita salud de cada app/analysis: dir existe, `.git` presente, manifest parseable, `.venv` valido (analyses), upstream configurado.
- `functions/infra/services_status` (`services_status_go_infra`, **impure**) — apps con tag `service` + `systemctl is-active` (user/system) + puerto declarado en notes/description + check TCP localhost.
- `functions/infra/pc_locations_drift` (`pc_locations_drift_go_infra`, **impure**) — detecta drift `pc_locations` BD vs disco para el PC actual (`~/.fn_pc`). Tres tipos: `missing_on_disk`, `untracked_on_disk`, `status_should_be_active`.
- `functions/infra/audit_uses_functions` (`audit_uses_functions_go_infra`, **impure**) — para cada app Go/Py compara imports reales contra `uses_functions` del `app.md`. Reporta `missing_in_app_md` y `unused_in_app_md`. Heuristica documentada (puede dar falsos positivos en `unused`).
- `functions/infra/find_unused_functions` (`find_unused_functions_go_infra`, **impure**) — funciones del registry sin consumidores en otras funciones, apps o analyses. Pipelines sin tag `launcher` tambien aparecen.
- `bash/functions/pipelines/backup_all` (`backup_all_bash_pipelines`, **impure**, tag `launcher`) — orquesta `backup_sqlite_db` + `rotate_backups` sobre `registry.db`, cada `apps/*/operations.db`, y rsync `--link-dest` para vaults declarados en `projects/*/vaults/vault.yaml`.
### Changed
- `.claude/CLAUDE.md` — seccion CLI ampliada con comandos `fn doctor [subcommand] [--json]` y enlace a la regla.
- `.claude/rules/INDEX.md` — anadida fila 23 para `fn_doctor.md`.
### Fixed
- `functions/infra/pc_locations_drift.go``filepath.Join(absoluto, absoluto)` producia paths corruptos cuando `dir_path` ya era absoluto (caso comun: filas `pc_locations` traen path absoluto al disco del PC). Fix: chequear `filepath.IsAbs` antes de unir. Sintoma previo: todos los artefactos reportados como `missing_on_disk` aunque existieran.
- `go.mod``golang.org/x/net` movido a deps directas (`go mod tidy` tras anadir `notify_telegram`).
### Notes
- Hallazgo de la primera ejecucion `fn doctor uses-functions`: 7/12 apps con drift real (`auto_metabase`, `dag_engine`, `deploy_server`, `docker_tui`, `kanban`, `metabase_registry`, `script_navegador`). Pendiente sincronizar sus `app.md` con los imports reales en sesion futura.
- `fn doctor unused` muestra muchas funciones core sin consumidores aun (`compose2_go_core`, `curry2_go_core`, etc.). Esperado: el registry crece antes que las apps que las consuman.
## 2026-05-04
### Added
- `cpp/functions/viz/graph_labels_select` (`graph_labels_select_cpp_viz`, **pure**) — TU separado de `graph_labels` con los helpers puros `graph_compute_degrees` y `graph_labels_select` (frustum cull + always_for_* + top-N por `size * (degree+1)`). Vive en su propio archivo para que los tests unitarios lo cubran sin abrir ImGui.
- `cpp/functions/viz/graph_viewport_selection` (`graph_viewport_selection_cpp_viz`, **pure**) — TU separado de `graph_viewport` con `clear_selection`, `is_selected`, `add_to_selection`, `toggle_selection`. Mantienen sincronizados `state.selection` y `nodes[i].flags & NF_SELECTED`.
- `cpp/functions/viz/graph_types` (`graph_types_cpp_viz`, **pure**) — TU de implementacion de `GraphData::update_bounds()` y `GraphData::find_node_by_user_data()`. Pareja obligatoria del header del tipo (`graph_types.h` indexado en `types/viz/`).
- `cpp/apps/chart_demo/app.md` — la demo de primitivos viz (line/scatter/bar/heatmap) ahora aparece en el registry como `chart_demo_cpp_viz`.
- `cpp/apps/shaders_lab/app.md` — el live GLSL playground con DAG ahora tiene `app.md` propio (antes solo existia entrada legacy en BD sin `.md` en disco).
### Changed
- `registry/indexer.go` — el indexer ahora escanea tambien `<lang>/apps/*/app.md` (mismo patron que ya usaba para `<lang>/functions/` y `<lang>/types/`). Antes solo veia `apps/` y `projects/*/apps/` — las apps en `cpp/apps/` quedaban invisibles. `./fn index` reporta 17 apps (antes 15).
- `cpp/functions/viz/graph_labels.md``signature` reducida a `graph_labels_draw` y `graph_labels_draw_at` (los helpers puros pasan a entrada propia). `uses_functions` apunta a la nueva entrada `graph_labels_select_cpp_viz`.
- `cpp/functions/viz/graph_viewport.md``uses_functions` añade `graph_viewport_selection_cpp_viz`.
- `projects/osint_graph/apps/graph_explorer/app.md``uses_functions` sincronizado con `CMakeLists.txt`: ahora declara las 23 funciones del registry que enlaza (antes 15). Añadidas: `graph_viewport_selection`, `graph_labels_select`, `graph_types`, `graph_spatial_hash`, `button`, `icon_button`, `badge`, `empty_state`.
- `projects/fn_monitoring/apps/registry_dashboard/app.md``uses_functions` sincronizado con `CMakeLists.txt` (21 deps, antes 9). Añadidas: `badge`, `button`, `empty_state`, `icon_button`, `modal_dialog`, `page_header`, `process_runner`, `process_state_machine`, `select`, `text_input`, `toast`, `toolbar`, `tree_view`. Removido: `fps_overlay` (vive en `fn_framework`, no se declara).
### Decisions
- ADR `0003-orphan-tu-as-separate-function-entry.md` — cuando una funcion del registry necesita partir su `.cpp` en varios TUs por testabilidad o separacion ImGui-vs-puro, cada TU adicional se registra como entrada propia con su `.md` en lugar de extender `file_path` para listar varios archivos. El parent declara la nueva entrada en `uses_functions`. Razon: el indexer asume `1 .cpp = 1 .md`; un `file_path` multi-archivo rompe la convencion y deja apps nuevas sin saber que TUs enlazar.
### Added — sesion NER+RE para graph_explorer (tarde, 980 → 990 funciones)
**18 funciones nuevas** sobre el ecosistema NER+RE, en dos rondas de `fn-constructor`:
Ronda 1 — extraccion de relaciones (mREBEL/REBEL/MarianMT):
- `python/functions/datascience/parse_rebel_output.py` (pure) — parser wire `<triplet>` REBEL/mREBEL.
- `python/functions/datascience/align_relations_to_entities.py` (pure) — string-match aligner.
- `python/functions/datascience/mrebel_load_model.py` (impure, **CC BY-NC-SA 4.0 — NO comercial**).
- `python/functions/datascience/mrebel_base_load_model.py` (impure, misma licencia).
- `python/functions/datascience/rebel_load_model.py` (impure, **Apache 2.0**, EN-only).
- `python/functions/datascience/marianmt_es_en_load_model.py` (impure) — Helsinki-NLP/opus-mt-es-en.
- `python/functions/datascience/translate_es_to_en.py` (impure) — wrapper traduccion frase a frase.
- `python/functions/datascience/extract_relations_mrebel.py` (impure) — pipeline mREBEL frase-a-frase + alineamiento.
- 21 tests pytest verdes.
Ronda 2 — pipeline GLiNER2 + OpenIE schema-less + composicion (tarde):
- `python/functions/core/clean_pdf_text.py` (pure) — limpia artefactos PyPDF2.
- `python/functions/core/chunk_with_overlap.py` (pure) — sliding window con avance forzado.
- `python/functions/core/merge_entity_aliases.py` (pure) — coreferencia normalize+substring.
- `python/functions/core/filter_relations_by_entity_types.py` (pure) — post-filter typed.
- `python/functions/core/aggregate_extraction_results.py` (pure) — dedupe + Counter sobre N chunks.
- `python/functions/datascience/gliner2_load_model.py` (impure, **Apache 2.0**) — `fastino/gliner2-large-v1`.
- `python/functions/datascience/extract_graph_gliner2.py` (impure) — wrapper schema + threshold + include_confidence.
- `python/functions/datascience/spacy_es_load_model.py` (impure) — `es_core_news_md` cacheado.
- `python/functions/datascience/extract_triples_spacy_es.py` (impure) — OpenIE schema-less ES por reglas de dependencia (verbo del texto = predicado).
- `python/functions/pipelines/extract_graph_from_text.py` (impure pipeline) — composicion E2E: chunk → extract_graph_gliner2 (×N) → aggregate → filter typed → merge aliases → grafo final.
- 39 tests pytest verdes.
### Added — analysis `gliner_glirel_tuning`
`projects/osint_graph/analysis/gliner_glirel_tuning/` — investigacion empirica de modelos NER/RE. **9 notebooks** ejecutados:
| # | Notebook | Hallazgo clave |
|---|---|---|
| 01 | `01_gliner_glirel_tuning.ipynb` | Calibracion de thresholds GLiNER+GLiREL |
| 02 | `02_e2e_spanish_graph.ipynb` | E2E texto ES — descubrimiento del fail de GLiREL en castellano |
| 03 | `03_mrebel_vs_glirel.ipynb` | mREBEL gana a GLiREL pero CC BY-NC-SA |
| 04 | `04_gliner2_winner.ipynb` ⭐ | **GLiNER2 (Apache 2.0, NER+RE joint, 340M)** elegido como motor principal |
| 05 | `05_long_text_and_pdf.ipynb` | Pipeline PDF E2E sobre `politica_proteccion_datos.pdf` (BBVA, 89.882 chars) |
| 06 | `06_improvements.ipynb` | Threshold 0.3 (vs default 0.5) → +187% relaciones; coref reduce 18% aislados |
| 07 | `07_nuextract_vs_gliner2.ipynb` | NuExtract GPU 2.6× mas lento, calidad similar — descartado por defecto |
| 08 | `08_improving_gliner2.ipynb` | snake_case verbal labels + post-filter typed = mejor combo |
| 09 | `09_spacy_es_openie.ipynb` | spaCy ES dep-rules: schema-less, predicado = verbo del texto |
### Added — vault `osint_nlp_models`
`projects/osint_graph/vaults/osint_nlp_models` (symlink a `~/vaults/osint_nlp_models/`):
- `models/` — fichas de gliner, glirel, mrebel, gliner2, candidates a probar.
- `decisions/` — 3 ADRs cortos del 2026-05-04 (mrebel-over-glirel mañana, gliner2-over-mrebel tarde, license-constraint).
- `benchmarks/corpus_v1.md` + `results_log.csv` (15 filas de experimentos).
- `test_documents/politica_proteccion_datos.pdf` (PDF de BBVA copiado para reproducibilidad).
### Added — playground HTML
`projects/osint_graph/analysis/gliner_glirel_tuning/playground/`:
- `server.py` — FastAPI con GLiNER2 cacheado, endpoints `GET /` (HTML) y `POST /extract` (texto → grafo).
- `index.html` — UI: textarea, KPIs (nodos/aristas/tiempo), grafo Sigma.js, JSON exportable.
- `static/sigma.min.js` + `graphology.umd.min.js` (servidos localmente para evitar bloqueo CDN por extensiones tipo MetaMask/SES).
Stack aplicado por el server:
1. snake_case verbal labels (`works_at`, `ceo_of`, `headquartered_in`, `agreement_with`...)
2. threshold 0.3 (configurable)
3. chunking automatico > 1500 chars
4. post-filter typed (`(person, organization)` validos por relacion)
5. coreferencia normalize+substring
6. layout server-side via `networkx.spring_layout`
7. render Sigma.js (sin fisica → sin loops de ResizeObserver)
### Added — issues
- `dev/issues/0050-jupyter-exec-collab-client-failure.md` — bug `jupyter_exec` con cliente colaborativo + workaround documentado.
- `projects/osint_graph/apps/graph_explorer/issues/0041-split-confidence-thresholds.md` — split `confidence_threshold` en `entity_threshold` + `relation_threshold`.
- `projects/osint_graph/apps/graph_explorer/issues/0042-gliner2-unified-extractor.md` ⭐ — sustituir GLiREL por GLiNER2 en `extract_graph_hybrid`. Reemplaza 0042-mrebel.
- `projects/osint_graph/apps/graph_explorer/issues/0042-mrebel-relation-extractor.md.superseded` — version mREBEL del 0042 archivada al ganar GLiNER2.
### Changed
- `cpp/CMakeLists.txt``_GE_DIR` y `_DASH_DIR` sobreescribibles via `-D<...>=<path>` para builds en worktrees (commit `e72d6364`). Habilita `parallel-fix-issues` sobre apps C++.
- `python/functions/datascience/glirel_load_model.py` — workaround compat `huggingface_hub` 1.x: classmethod monkey-patch idempotente para inyectar `proxies`/`resume_download` que el HF nuevo dejo de pasar (commit `3b3378cf`).
- Sub-repo `dataforge/graph_explorer` master local: merges `--no-ff` de `issue/0035e-polish-and-tests` (commit `f614a51`) + `issue/0013-paste-extract-panel` (commit `2a49c2b`). 125/125 tests pytest verdes. **Sin push aun** — pendiente confirmacion + validacion Windows.
### Fixed (bugs encontrados + raiz + fix)
| Bug | Raiz | Fix |
|---|---|---|
| `chunk_with_overlap` bucle infinito | Frase mas larga que `max_chars`, no avanzaba `i`, OOM-killed por overlap acumulado | Avance forzado: meter al menos UNA frase aunque exceda `max_chars` |
| NuExtract degenera en texto largo | Sin `repetition_penalty`, decoder entra en bucle de tokens repetidos hasta agotar 2048 max_new_tokens | `repetition_penalty=1.15` + chunking obligatorio (179/179 chunks parsed OK tras fix) |
| NuExtract `AutoProcessor.from_pretrained` rota en transformers 5.x | Sub-processor de video tira `TypeError: argument of type 'NoneType' is not iterable` (Qwen2-VL) | Bypass: `AutoTokenizer` + `AutoModelForImageTextToText` directamente |
| Vis-network ResizeObserver loop spam (en SES/MetaMask) | Vis-network usa physics simulation → ResizeObserver dispara warnings amplificados por SES | Migrar a Sigma.js + layout server-side via `networkx.spring_layout` (sin fisica frontend) |
| `jupyter_exec append` HTTP 405 | `jupyter_nbmodel_client` espera collab WebSocket Y.js, no soportado al 100% por jupyter-collaboration nuevo | Documentado en issue 0050; workaround actual: build_notebook scripts con `nbformat` + `nbconvert --execute` |
| Kernel startup shadows pip packages | `00_fn_registry.py` añade cada subdir de `python/functions/` a sys.path top-level → `bigquery/datasets.py` shadows HF `datasets` package needed by transformers | Workaround per-notebook: `sys.path = [p for p in sys.path if not p.startswith(_pf+'/')]` + añadir solo el padre. Issue futuro pendiente. |
### Decisions — vault ADRs
| Decision | Razon |
|---|---|
| **GLiNER2 (Apache 2.0)** sustituye a GLiREL en `extract_graph_hybrid` | 6/8 relaciones correctas vs 0/1 de GLiREL en es_corporate_short, 1.18s vs 22s de mREBEL, NER+RE en una pasada |
| mREBEL queda como fallback (no comercial) | 4/5 correctas pero CC BY-NC-SA 4.0 + 25× mas lento |
| spaCy ES dep-rules para OpenIE schema-less | Predicado = verbo del texto (`querer`, `abrazar`), 5ms/frase, sin alucinaciones |
| Threshold `0.3` (vs default `0.5`) sweet spot | +187% relaciones manteniendo precision; 0.2 mete +22% entidades dudosas |
| Coreferencia normalize+substring + post-filter typed = **gratis y decisivos** | Coref 18% aislados; post-filter elimina `Madrid president_of Persona` |
| Translate ES→EN + triplet-extract EN **NO** vale la pena | Pierdes verbos del texto (`querer``loves`), +500ms-1s, +300MB MarianMT, riesgo nombres propios |
## 2026-04-28
### Added
- `cpp/functions/core/app_about` (`app_about_cpp_core`) — ventana flotante About con `about_window_set_info(project, version, description)`, `about_window_menu_item("About...")` y `about_window_render()`. Render automatico via `fn::run_app` (cableado en `cpp/framework/app_base.cpp`).
- `bash/functions/infra/ensure_repo_synced` (`ensure_repo_synced_bash_infra`) — pipeline idempotente que compone `gitea_create_repo` + `gitea_push_directory`: crea repo Gitea si falta, inicializa `.git` local si falta, commitea cambios pendientes y pushea. Defaults: owner `dataforge`, branch `master`.
- `analysis.md` para 6 analyses que estaban en disco pero sin indexar: `agent_coding_eval`, `estudio_embeddings`, `estudio_mercados`, `ontology_graph`, `pruebas_jupyter`, `retrieving_graphs`. Ahora `./fn index` reporta 8 analyses (antes 2).
- Repos `dataforge/<name>` creados en Gitea para apps y analyses que no estaban subidos: `agents_and_robots`, `element_matrix_chat`, `deploy_server`, `shaders_lab`, `voice_guide`, `agent_coding_eval`, `ontology_graph`, `turismo_spain`. Cada uno con `.gitignore` apropiado para excluir binarios, `.venv/`, `node_modules/`, `.jupyter*`, `operations.db*`.
### Changed
- `cpp/functions/core/app_menubar`: el item top-level `Settings...` pasa a ser un `BeginMenu("Settings")` con dos subitems: `Settings...` (ventana de `app_settings`) y `About...` (nuevo, ventana de `app_about`). Las apps que usan `fn_ui::app_menubar(nullptr, 0, nullptr)` heredan el cambio sin tocar nada.
- `projects/fn_monitoring/apps/registry_dashboard/main.cpp`: cablea `fn_ui::about_window_set_info("fn_registry Dashboard", "0.2.0", "...")` antes de `fn::run_app`. Tabla `Apps` gana columna `Git` con valores `remote` (repo_url poblado), `local` (.git/ presente) o `-`.
- `data.h`/`data.cpp`/`data_http.cpp` del dashboard: `AppRow` extendido con `repo_url` y `dir_path`.
- 10 repos migrados de branch `main` a `master` para unificar convencion: `apps/{docker_tui,fuzzygraph,metabase_registry,pipeline_launcher,rapid_dashboards,script_navegador}`, `analysis/{estudio_embeddings,estudio_mercados,pruebas_jupyter,retrieving_graphs}`. Default branch en Gitea actualizado via API (`PATCH /repos/{owner}/{repo}` con `{"default_branch":"master"}`), branch `main` remota borrada.
- `git config --global init.defaultBranch master` para que los proximos `git init` sean consistentes.
- `/full-git-push`: descubre apps/analyses sin `.git` y ofrece inicializarlos con `ensure_repo_synced` automaticamente. Excluye `subrepos/` para evitar duplicacion (mirrors upstream).
- `/full-git-pull`: tras `fn sync`, segunda pasada que clona los `dataforge/<name>` registrados en `apps`/`analysis` que no existan localmente — soluciona el "no pude recuperar la app en el otro PC".
- `bash/functions/infra/ensure_repo_synced.sh`: localiza dependencias via `FN_REGISTRY_INFRA_DIR` o `FN_REGISTRY_ROOT`, robusto a sourcing desde zsh/bash.
### Fixed
- `projects/fn_monitoring/apps/sqlite_api/handlers.go|main.go|handlers_test.go` + nuevos `handlers_mutations.go` y `handlers_projects.go`: cableados endpoints `POST /add_app|add_analysis|add_vault|reindex` y `GET /projects` para que el dashboard pueda crear artefactos y navegar projects desde la actions bar (estado pendiente de varios dias en uncommitted, ahora versionado en `dataforge/sqlite_api`).
- Bug operativo en `sqlite_api` (Windows): `SO_RCVTIMEO` se pasaba como `struct timeval` cuando Windows espera `DWORD ms` → timeout efectivo de 5 ms. Ya documentado en `app.md` del dashboard.
## 2026-04-24
### Added
- 6 funciones `bash/infra/systemd_local_*` (install_unit, enable, start, restart, status, uninstall) para gestionar servicios systemd del sistema desde el registry (complementa las versiones remotas SSH ya existentes).
- Pipeline `install_systemd_service_bash_pipelines` que compone las anteriores: genera unit file + install + enable + start + status.
- Servicio systemd `sqlite_api.service` instalado y habilitado en aurgi-pc — arranque automático al iniciar WSL en `127.0.0.1:8484`.
- `projects/fn_monitoring/launcher.sh` — launcher del dashboard (arranca API si no está + lanza ventana + cleanup).
- Regla [`.claude/rules/kiss.md`](.claude/rules/kiss.md) — filosofía KISS para proyectos y apps.
- Documentación ADR en `docs/adr/` con plantilla y ADR 0001 (experimento GitButler).
- Diario en `docs/diary/` + slash command `/entrada_diario` para añadir entradas.
- `CHANGELOG.md` (este archivo).
- Submódulo `cpp/vendor/glfw` re-registrado con path limpio (antes heredado con path absoluto `/home/lucas/...`).
- aurgi-pc registrado en el server centralizado (`registry.organic-machine.com`) con 18 pc_locations.
### Changed
- `registry.db` ahora está gitignorada. Es regenerable con `fn index` + completable con `fn sync`. Evita conflictos entre ramas y PCs.
- `sqlite_api` ahora se distribuye como binario compilado (`projects/fn_monitoring/apps/sqlite_api/sqlite_api`) en lugar de `go run` al vuelo.
### Fixed
- `http_client.cpp` del dashboard: añadido `#include <cstdint>` requerido por mingw-w64 para cross-compile Windows (g++ Linux lo incluía transitivamente).
- `registry_dashboard.exe` (Windows) ya no abre ventana de consola al lanzarse — enlazado como GUI app (`WIN32_EXECUTABLE TRUE` / `-mwindows`).
### Added (design system C++)
- `cpp/functions/core/tokens` — design tokens para dashboards ImGui (colors, spacing, radius, font_size) inspirados en `@fn_library` (Mantine v9). Paleta dark + indigo primary. `apply_dark_theme()` aplica los tokens al `ImGuiStyle` global.
- `cpp/functions/core/badge` — etiqueta inline con 6 variantes (Default/Success/Warning/Error/Info/Outline). Equivalente a `<Badge>` de `@fn_library`.
- `cpp/functions/core/empty_state` — placeholder centrado para tablas/listas vacías.
- `cpp/functions/core/page_header` — header de página con título/subtítulo + hueco para acciones + separator.
- `registry_dashboard` migrado a los nuevos componentes: `page_header_begin/end` en el header, `empty_state` en las 4 tablas cuando están vacías, `apply_dark_theme()` al primer frame. Sin hardcode de colores disperso.
- `systemd_local_{enable,start,restart}`: stdout de `systemctl` redirigido a stderr para no contaminar el JSON capturado por el pipeline.
- `.gitmodules`: entry fantasma `cpp/vendor/glfw` con path absoluto `/home/lucas/...` que bloqueaba `git submodule status` y el cross-compile Windows.
### Removed
- Integración de GitButler de Claude Code — binario `~/.local/bin/but`, plugin `gitbutler-tools`, skill `.claude/skills/gitbutler/`, hooks en `settings.json`, ramas `gitbutler/*` + `e-branch-*`, estado interno `.git/gitbutler/`. Ver [ADR 0001](docs/adr/0001-gitbutler-experiment.md) para motivos.
View File
+18
View File
@@ -0,0 +1,18 @@
# Build output
dag_engine
*.exe
# Frontend build
frontend/dist/
frontend/node_modules/
# Go
vendor/
# Editor
.idea/
.vscode/
*.swp
# OS
.DS_Store
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"io/fs"
"net/http"
)
// RegisterAPI sets up all HTTP routes on the given mux.
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, frontendFS fs.FS) {
// API routes.
mux.HandleFunc("GET /api/dags", handleListDags(executor))
mux.HandleFunc("GET /api/dags/{name}", handleGetDag(executor))
mux.HandleFunc("POST /api/dags/{name}/run", handleRunDag(executor))
mux.HandleFunc("GET /api/runs", handleListRuns(executor))
mux.HandleFunc("GET /api/runs/{id}", handleGetRun(executor))
mux.HandleFunc("POST /api/scheduler/start", handleSchedulerStart(scheduler))
mux.HandleFunc("POST /api/scheduler/stop", handleSchedulerStop(scheduler))
mux.HandleFunc("GET /api/scheduler/status", handleSchedulerStatus(scheduler))
// Frontend SPA fallback.
if frontendFS != nil {
mux.Handle("/", spaHandler(frontendFS))
}
}
// spaHandler serves static files from the embedded FS, falling back to index.html
// for unknown paths (SPA client-side routing).
func spaHandler(fsys fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Try to serve the file directly.
path := r.URL.Path
if path == "/" {
path = "index.html"
} else {
path = path[1:] // strip leading /
}
if _, err := fs.Stat(fsys, path); err != nil {
// File not found — serve index.html for SPA routing.
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
+85
View File
@@ -0,0 +1,85 @@
---
name: dag_engine
lang: go
domain: infra
description: "Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite."
tags: [service, dag, workflow, scheduler, web, cron]
uses_functions:
- dag_parse_go_core
- dag_validate_go_core
- dag_topo_sort_go_core
- dag_resolve_env_go_core
- parse_cron_expr_go_core
- next_cron_time_go_core
- cron_ticker_go_infra
- find_go_core
- process_spawn_go_infra
- process_wait_go_infra
uses_types:
- dag_definition_go_core
- dag_step_go_core
- dag_validation_result_go_core
- cron_schedule_go_core
- process_handle_go_infra
- process_result_go_infra
- DagRun_go_infra
- DagStepResult_go_infra
framework: "net/http + vite + react"
entry_point: "main.go"
dir_path: "apps/dag_engine"
---
## Arquitectura
CLI + servidor web en un unico binario:
```
dag-engine run <path.yaml> # ejecuta un DAG desde terminal
dag-engine list [dir] # lista DAGs con schedule y estado
dag-engine status [dag_name] # historial de ejecuciones
dag-engine validate <path.yaml> # valida sin ejecutar
dag-engine server # arranca HTTP + frontend web
```
### Backend (Go)
- `net/http` con `ServeMux` (Go 1.22+ pattern routing)
- SQLite via `go-sqlite3` para historial de runs
- Executor: parse -> validate -> topo_sort -> spawn/wait por nivel -> store
- Scheduler: cron_ticker por cada DAG con schedule
### Frontend (Vite + React + Mantine)
- DagList: tabla de DAGs con schedule, tags, ultimo status
- DagDetail: metadata + "Run Now" + historial
- RunDetail: timeline de steps con stdout/stderr expandible
### Storage
SQLite `dag_engine.db`:
- `dag_runs`: id, dag_name, status, trigger, started_at, finished_at, error
- `dag_step_results`: id, run_id, step_name, status, exit_code, stdout, stderr, duration_ms
### Build
```bash
cd frontend && pnpm install && pnpm build
cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
```
### Uso
```bash
# CLI
./dag-engine run ~/dagu/dags/example.yaml
./dag-engine list ~/dagu/dags/
# Servidor web
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
# Browser: http://localhost:8090
```
## Notas
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
Puerto por defecto 8090 (mismo que Dagu).
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"flag"
"os"
"path/filepath"
)
// Config holds the runtime configuration for the DAG engine.
type Config struct {
Port int
DagsDir string
DBPath string
AutoScheduler bool
}
// DefaultConfig returns sensible defaults.
func DefaultConfig() Config {
home, _ := os.UserHomeDir()
return Config{
Port: 8090,
DagsDir: filepath.Join(home, "dagu", "dags"),
DBPath: "dag_engine.db",
}
}
// ParseFlags populates config from CLI flags for the "server" subcommand.
func (c *Config) ParseFlags(fs *flag.FlagSet, args []string) error {
fs.IntVar(&c.Port, "port", c.Port, "HTTP server port")
fs.StringVar(&c.DagsDir, "dags-dir", c.DagsDir, "directory containing DAG YAML files")
fs.StringVar(&c.DBPath, "db", c.DBPath, "path to SQLite database")
fs.BoolVar(&c.AutoScheduler, "scheduler", c.AutoScheduler, "auto-start cron scheduler")
return fs.Parse(args)
}
+482
View File
@@ -0,0 +1,482 @@
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"fn-registry/functions/core"
"fn-registry/functions/infra"
"dag-engine/store"
)
// Executor orchestrates DAG parsing, validation, and execution.
type Executor struct {
store *store.DB
dagsDir string
}
// NewExecutor creates a new executor.
func NewExecutor(s *store.DB, dagsDir string) *Executor {
return &Executor{store: s, dagsDir: dagsDir}
}
// ExecuteDAG runs a DAG from a YAML file path and returns the run ID.
// It runs asynchronously: steps execute in topological order with parallel levels.
func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger string) (string, error) {
data, err := os.ReadFile(dagPath)
if err != nil {
return "", fmt.Errorf("read dag: %w", err)
}
dag, err := core.DagParse(data)
if err != nil {
return "", fmt.Errorf("parse dag: %w", err)
}
dag.FilePath = dagPath
// Resolve env variables.
dag = core.DagResolveEnv(dag, os.Environ())
// Validate.
result := core.DagValidate(dag)
if !result.Valid {
return "", fmt.Errorf("validate dag: %s", strings.Join(result.Errors, "; "))
}
// Create run record.
runID := generateID()
now := time.Now()
run := &store.DagRun{
ID: runID,
DagName: dag.Name,
DagPath: dagPath,
Status: "running",
Trigger: trigger,
StartedAt: now,
}
if err := e.store.CreateRun(run); err != nil {
return "", fmt.Errorf("create run: %w", err)
}
// Topological sort.
levels, err := core.DagTopoSort(dag.Steps)
if err != nil {
e.failRun(runID, err)
return runID, err
}
// Setup DAGU_ENV temp file for inter-step communication.
daguEnvFile, err := os.CreateTemp("", "dagu_env_*")
if err != nil {
e.failRun(runID, err)
return runID, err
}
daguEnvPath := daguEnvFile.Name()
daguEnvFile.Close()
defer os.Remove(daguEnvPath)
// Track step outputs for ${step_id.stdout} references.
stepOutputs := make(map[string]string)
// Execute levels.
runFailed := false
var runErr error
for _, level := range levels {
if runFailed {
// Skip remaining levels, mark steps as skipped.
for _, step := range level {
e.recordStepSkipped(runID, step)
}
continue
}
var wg sync.WaitGroup
var mu sync.Mutex
levelFailed := false
for _, step := range level {
step := step
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
if levelFailed {
mu.Unlock()
e.recordStepSkipped(runID, step)
return
}
mu.Unlock()
err := e.executeStep(ctx, runID, dag, step, daguEnvPath, stepOutputs, &mu)
if err != nil && !step.ContinueOn.Failure {
mu.Lock()
levelFailed = true
runFailed = true
runErr = fmt.Errorf("step %q failed: %w", stepName(step), err)
mu.Unlock()
}
}()
}
wg.Wait()
}
// Run handlers.
if runFailed {
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Failure, daguEnvPath, stepOutputs)
} else {
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Success, daguEnvPath, stepOutputs)
}
e.runHandlers(ctx, runID, dag, dag.HandlerOn.Exit, daguEnvPath, stepOutputs)
// Finalize run.
fin := time.Now()
status := "success"
errMsg := ""
if runFailed {
status = "failed"
if runErr != nil {
errMsg = runErr.Error()
}
}
e.store.UpdateRunStatus(runID, status, &fin, errMsg)
return runID, runErr
}
// executeStep runs a single step, recording results in the store.
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
stepID := generateID()
now := time.Now()
e.store.InsertStepResult(&store.DagStepResult{
ID: stepID,
RunID: runID,
StepName: stepName(step),
Status: "running",
StartedAt: &now,
})
// Build environment.
env := buildStepEnv(dag, step, daguEnvPath, outputs)
// Determine command.
command := step.Command
if command == "" && step.Script != "" {
command = step.Script
}
if command == "" {
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
return nil
}
// Resolve step-level ${VAR} references and ${step_id.stdout} patterns.
mu.Lock()
command = resolveStepRefs(command, outputs)
mu.Unlock()
// Determine working directory.
dir := step.Dir
if dir == "" {
dir = dag.WorkingDir
}
shell := step.Shell
if shell == "" {
shell = dag.Shell
}
// Spawn process.
handle, err := infra.ProcessSpawn(command, dir, env, shell)
if err != nil {
fin := time.Now()
e.store.UpdateStepResult(stepID, "failed", -1, "", "", &fin, time.Since(now).Milliseconds(), err.Error())
return err
}
// Wait for process.
result, err := infra.ProcessWait(handle, step.TimeoutSec)
fin := time.Now()
duration := time.Since(now).Milliseconds()
if err != nil && result.ExitCode == 0 {
result.ExitCode = -1
}
status := "success"
errMsg := ""
if result.ExitCode != 0 || err != nil {
status = "failed"
if err != nil {
errMsg = err.Error()
}
}
e.store.UpdateStepResult(stepID, status, result.ExitCode, result.Stdout, result.Stderr, &fin, duration, errMsg)
// Store output for ${step_id.stdout} references.
if step.ID != "" || step.Output != "" {
mu.Lock()
key := step.ID
if key == "" {
key = step.Output
}
outputs[key] = strings.TrimSpace(result.Stdout)
mu.Unlock()
}
// Read DAGU_ENV for inter-step env propagation.
readDaguEnv(daguEnvPath, outputs)
if status == "failed" {
return fmt.Errorf("exit code %d", result.ExitCode)
}
return nil
}
func (e *Executor) runHandlers(ctx context.Context, runID string, dag core.DagDefinition, handlers []core.DagStep, daguEnvPath string, outputs map[string]string) {
var mu sync.Mutex
for _, step := range handlers {
e.executeStep(ctx, runID, dag, step, daguEnvPath, outputs, &mu)
}
}
func (e *Executor) failRun(runID string, err error) {
fin := time.Now()
e.store.UpdateRunStatus(runID, "failed", &fin, err.Error())
}
func (e *Executor) recordStepSkipped(runID string, step core.DagStep) {
now := time.Now()
e.store.InsertStepResult(&store.DagStepResult{
ID: generateID(),
RunID: runID,
StepName: stepName(step),
Status: "skipped",
StartedAt: &now,
})
}
// --- helpers ---
func stepName(s core.DagStep) string {
if s.Name != "" {
return s.Name
}
return s.ID
}
func buildStepEnv(dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string) []string {
env := os.Environ()
// Add DAG-level env.
for k, v := range dag.Env {
env = append(env, k+"="+v)
}
// Add step-level env.
for k, v := range step.Env {
env = append(env, k+"="+v)
}
// Add DAGU_ENV path.
env = append(env, "DAGU_ENV="+daguEnvPath)
return env
}
func resolveStepRefs(command string, outputs map[string]string) string {
for k, v := range outputs {
command = strings.ReplaceAll(command, "${"+k+".stdout}", v)
command = strings.ReplaceAll(command, "$"+k+".stdout", v)
}
return command
}
func readDaguEnv(path string, outputs map[string]string) {
data, err := os.ReadFile(path)
if err != nil || len(data) == 0 {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
outputs[parts[0]] = parts[1]
}
}
}
// generateID creates a simple time-based unique ID.
func generateID() string {
return fmt.Sprintf("%d-%04x", time.Now().UnixNano(), time.Now().Nanosecond()%0xFFFF)
}
// --- DAG listing helpers ---
// DagInfo summarizes a DAG file for listing.
type DagInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Schedule []string `json:"schedule,omitempty"`
Tags []string `json:"tags,omitempty"`
Type string `json:"type,omitempty"`
FilePath string `json:"file_path"`
Valid bool `json:"valid"`
LastRun *store.DagRun `json:"last_run,omitempty"`
}
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
func (e *Executor) ListDAGs() ([]DagInfo, error) {
entries, err := os.ReadDir(e.dagsDir)
if err != nil {
return nil, fmt.Errorf("read dags dir: %w", err)
}
var dags []DagInfo
for _, entry := range entries {
if entry.IsDir() {
continue
}
ext := filepath.Ext(entry.Name())
if ext != ".yaml" && ext != ".yml" {
continue
}
path := filepath.Join(e.dagsDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
continue
}
dag, err := core.DagParse(data)
if err != nil {
dags = append(dags, DagInfo{
Name: strings.TrimSuffix(entry.Name(), ext),
FilePath: path,
Valid: false,
})
continue
}
info := DagInfo{
Name: dag.Name,
Description: dag.Description,
Schedule: dag.Schedule,
Tags: dag.Tags,
Type: dag.Type,
FilePath: path,
Valid: true,
}
// Attach last run info.
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
if len(runs) > 0 {
info.LastRun = &runs[0]
}
dags = append(dags, info)
}
return dags, nil
}
// GetDAG returns detailed info for a specific DAG by name.
func (e *Executor) GetDAG(name string) (*DagInfo, *core.DagDefinition, *core.DagValidationResult, error) {
// Find the YAML file.
entries, err := os.ReadDir(e.dagsDir)
if err != nil {
return nil, nil, nil, err
}
for _, entry := range entries {
ext := filepath.Ext(entry.Name())
base := strings.TrimSuffix(entry.Name(), ext)
if (ext != ".yaml" && ext != ".yml") || base != name {
continue
}
path := filepath.Join(e.dagsDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, nil, err
}
dag, err := core.DagParse(data)
if err != nil {
return nil, nil, nil, fmt.Errorf("parse: %w", err)
}
dag.FilePath = path
validationResult := core.DagValidate(dag)
info := &DagInfo{
Name: dag.Name,
Description: dag.Description,
Schedule: dag.Schedule,
Tags: dag.Tags,
Type: dag.Type,
FilePath: path,
Valid: validationResult.Valid,
}
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
if len(runs) > 0 {
info.LastRun = &runs[0]
}
return info, &dag, &validationResult, nil
}
return nil, nil, nil, fmt.Errorf("dag %q not found in %s", name, e.dagsDir)
}
// ValidateDAG parses and validates a DAG file, printing results.
func ValidateDAG(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
dag, err := core.DagParse(data)
if err != nil {
return fmt.Errorf("parse error: %w", err)
}
result := core.DagValidate(dag)
log.Printf("DAG: %s", dag.Name)
log.Printf("Steps: %d", len(dag.Steps))
log.Printf("Schedule: %v", dag.Schedule)
if result.Valid {
log.Printf("Validation: PASS")
log.Printf("Topological levels: %d", len(result.Levels))
for i, level := range result.Levels {
log.Printf(" Level %d: %v", i, level)
}
} else {
log.Printf("Validation: FAIL")
for _, e := range result.Errors {
log.Printf(" ERROR: %s", e)
}
}
for _, w := range result.Warnings {
log.Printf(" WARNING: %s", w)
}
if !result.Valid {
return fmt.Errorf("validation failed")
}
return nil
}
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAG Engine</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
{
"name": "dag-engine-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@mantine/core": "^9.0.2",
"@mantine/hooks": "^9.0.2",
"@tabler/icons-react": "^3.31.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1"
},
"devDependencies": {
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
"postcss": "^8.5.4",
"postcss-preset-mantine": "^1.17.0",
"typescript": "~5.8.3",
"vite": "^6.3.5"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"postcss-preset-mantine": {},
},
};
+32
View File
@@ -0,0 +1,32 @@
import { Routes, Route } from "react-router-dom";
import { AppShell, Container, Title, Group, Text } from "@mantine/core";
import { IconTopologyRing } from "@tabler/icons-react";
import { DagList } from "./pages/DagList";
import { DagDetail } from "./pages/DagDetail";
import { RunDetail } from "./pages/RunDetail";
export function App() {
return (
<AppShell header={{ height: 50 }} padding="md">
<AppShell.Header>
<Group h="100%" px="md">
<IconTopologyRing size={24} />
<Title order={4}>DAG Engine</Title>
<Text size="xs" c="dimmed">
fn_registry workflow executor
</Text>
</Group>
</AppShell.Header>
<AppShell.Main>
<Container size="lg">
<Routes>
<Route path="/" element={<DagList />} />
<Route path="/dags/:name" element={<DagDetail />} />
<Route path="/runs/:id" element={<RunDetail />} />
</Routes>
</Container>
</AppShell.Main>
</AppShell>
);
}
+63
View File
@@ -0,0 +1,63 @@
import type {
DagSummary,
DagDetail,
DagRun,
RunDetail,
SchedulerStatus,
} from "./types";
const BASE = "/api";
async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, init);
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || res.statusText);
}
return res.json();
}
export function listDags(): Promise<DagSummary[]> {
return fetchJSON("/dags");
}
export function getDag(name: string): Promise<DagDetail> {
return fetchJSON(`/dags/${encodeURIComponent(name)}`);
}
export function triggerDag(
name: string
): Promise<{ status: string; dag: string; message: string }> {
return fetchJSON(`/dags/${encodeURIComponent(name)}/run`, {
method: "POST",
});
}
export function listRuns(params?: {
dag?: string;
limit?: number;
offset?: number;
}): Promise<{ runs: DagRun[]; total: number }> {
const search = new URLSearchParams();
if (params?.dag) search.set("dag", params.dag);
if (params?.limit) search.set("limit", String(params.limit));
if (params?.offset) search.set("offset", String(params.offset));
const qs = search.toString();
return fetchJSON(`/runs${qs ? "?" + qs : ""}`);
}
export function getRun(id: string): Promise<RunDetail> {
return fetchJSON(`/runs/${encodeURIComponent(id)}`);
}
export function startScheduler(): Promise<void> {
return fetchJSON("/scheduler/start", { method: "POST" });
}
export function stopScheduler(): Promise<void> {
return fetchJSON("/scheduler/stop", { method: "POST" });
}
export function getSchedulerStatus(): Promise<SchedulerStatus> {
return fetchJSON("/scheduler/status");
}
@@ -0,0 +1,18 @@
import { Badge } from "@mantine/core";
const colorMap: Record<string, string> = {
success: "green",
failed: "red",
running: "blue",
pending: "gray",
cancelled: "yellow",
skipped: "dimmed",
};
export function StatusBadge({ status }: { status: string }) {
return (
<Badge color={colorMap[status] || "gray"} variant="light" size="sm">
{status}
</Badge>
);
}
@@ -0,0 +1,85 @@
import { Timeline, Text, Code, Collapse, Box, Group } from "@mantine/core";
import {
IconCircleCheck,
IconCircleX,
IconLoader,
IconCircleMinus,
IconClock,
} from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";
import type { DagStepResult } from "../types";
const iconMap: Record<string, React.ReactNode> = {
success: <IconCircleCheck size={16} color="var(--mantine-color-green-6)" />,
failed: <IconCircleX size={16} color="var(--mantine-color-red-6)" />,
running: <IconLoader size={16} color="var(--mantine-color-blue-6)" />,
skipped: <IconCircleMinus size={16} color="var(--mantine-color-dimmed)" />,
pending: <IconClock size={16} color="var(--mantine-color-gray-6)" />,
};
function StepItem({ step }: { step: DagStepResult }) {
const [opened, { toggle }] = useDisclosure(step.Status === "failed");
const hasOutput = step.Stdout || step.Stderr;
return (
<Timeline.Item
bullet={iconMap[step.Status] || iconMap.pending}
title={
<Group gap="xs">
<Text
size="sm"
fw={500}
onClick={hasOutput ? toggle : undefined}
style={hasOutput ? { cursor: "pointer" } : undefined}
>
{step.StepName}
</Text>
<Text size="xs" c="dimmed">
{step.DurationMs}ms
</Text>
{step.ExitCode !== 0 && step.ExitCode !== -1 && (
<Text size="xs" c="red">
exit {step.ExitCode}
</Text>
)}
</Group>
}
>
{hasOutput && (
<Collapse in={opened}>
<Box mt="xs">
{step.Stdout && (
<Code block mb="xs" style={{ maxHeight: 200, overflow: "auto" }}>
{step.Stdout}
</Code>
)}
{step.Stderr && (
<Code
block
color="red"
style={{ maxHeight: 200, overflow: "auto" }}
>
{step.Stderr}
</Code>
)}
</Box>
</Collapse>
)}
</Timeline.Item>
);
}
export function StepTimeline({ steps }: { steps: DagStepResult[] }) {
const activeIndex = steps.findIndex((s) => s.Status === "running");
return (
<Timeline
active={activeIndex >= 0 ? activeIndex : steps.length - 1}
bulletSize={24}
>
{steps.map((step) => (
<StepItem key={step.ID} step={step} />
))}
</Timeline>
);
}
+18
View File
@@ -0,0 +1,18 @@
import "@mantine/core/styles.css";
import { MantineProvider, createTheme } from "@mantine/core";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { App } from "./App";
const theme = createTheme({
primaryColor: "blue",
fontFamily: "system-ui, -apple-system, sans-serif",
});
createRoot(document.getElementById("root")!).render(
<MantineProvider theme={theme} defaultColorScheme="dark">
<BrowserRouter>
<App />
</BrowserRouter>
</MantineProvider>
);
@@ -0,0 +1,204 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
Title,
Text,
Group,
Button,
Badge,
Stack,
Paper,
Table,
Alert,
Loader,
Code,
} from "@mantine/core";
import { IconPlayerPlay, IconArrowLeft } from "@tabler/icons-react";
import { getDag, triggerDag } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import type { DagDetail as DagDetailType } from "../types";
export function DagDetail() {
const { name } = useParams<{ name: string }>();
const navigate = useNavigate();
const [data, setData] = useState<DagDetailType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [triggering, setTriggering] = useState(false);
const load = async () => {
if (!name) return;
setLoading(true);
try {
setData(await getDag(name));
setError(null);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, [name]);
const handleRun = async () => {
if (!name) return;
setTriggering(true);
try {
await triggerDag(name);
setTimeout(load, 1000);
} catch (e) {
setError((e as Error).message);
} finally {
setTriggering(false);
}
};
if (loading) return <Loader />;
if (error) return <Alert color="red">{error}</Alert>;
if (!data) return <Text>Not found</Text>;
const { dag, validation, runs } = data;
return (
<Stack gap="md">
<Group>
<Button
variant="subtle"
size="xs"
leftSection={<IconArrowLeft size={14} />}
onClick={() => navigate("/")}
>
Back
</Button>
</Group>
<Group justify="space-between">
<div>
<Title order={2}>{dag.Name}</Title>
{dag.Description && (
<Text size="sm" c="dimmed">
{dag.Description}
</Text>
)}
</div>
<Button
leftSection={<IconPlayerPlay size={16} />}
onClick={handleRun}
loading={triggering}
>
Run Now
</Button>
</Group>
<Group gap="xs">
{dag.Schedule?.map((s: string) => (
<Badge key={s} variant="light" ff="monospace">
{s}
</Badge>
))}
<Badge variant="light">{dag.Type || "chain"}</Badge>
{dag.Tags?.map((t: string) => (
<Badge key={t} variant="dot">
{t}
</Badge>
))}
</Group>
{!validation.Valid && (
<Alert color="red" title="Validation errors">
{validation.Errors.map((e: string, i: number) => (
<Text key={i} size="sm">
{e}
</Text>
))}
</Alert>
)}
<Paper p="md" withBorder>
<Title order={4} mb="sm">
Steps ({dag.Steps?.length || 0})
</Title>
{validation.Levels?.map((level: string[], i: number) => (
<Group key={i} gap="xs" mb="xs">
<Text size="xs" c="dimmed" w={60}>
Level {i}:
</Text>
{level.map((name: string) => {
const step = dag.Steps?.find(
(s) => s.Name === name || s.ID === name
);
return (
<Badge key={name} variant="outline" size="sm">
{name}
{step?.Depends?.length
? ` (after ${step.Depends.join(",")})`
: ""}
</Badge>
);
})}
</Group>
))}
{dag.Env && Object.keys(dag.Env).length > 0 && (
<>
<Title order={5} mt="md" mb="xs">
Environment
</Title>
<Code block>
{Object.entries(dag.Env)
.map(([k, v]) => `${k}=${v}`)
.join("\n")}
</Code>
</>
)}
</Paper>
<Paper p="md" withBorder>
<Title order={4} mb="sm">
Run History
</Title>
{runs?.length ? (
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Status</Table.Th>
<Table.Th>Trigger</Table.Th>
<Table.Th>Started</Table.Th>
<Table.Th>Duration</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{runs.map((r) => (
<Table.Tr
key={r.ID}
style={{ cursor: "pointer" }}
onClick={() => navigate(`/runs/${r.ID}`)}
>
<Table.Td>
<StatusBadge status={r.Status} />
</Table.Td>
<Table.Td>{r.Trigger}</Table.Td>
<Table.Td>
{new Date(r.StartedAt).toLocaleString()}
</Table.Td>
<Table.Td>
{r.FinishedAt
? `${Math.round((new Date(r.FinishedAt).getTime() - new Date(r.StartedAt).getTime()) / 1000)}s`
: "running..."}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
) : (
<Text size="sm" c="dimmed">
No runs yet
</Text>
)}
</Paper>
</Stack>
);
}
@@ -0,0 +1,164 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Table,
Title,
Group,
Button,
Badge,
Text,
Loader,
Stack,
Alert,
} from "@mantine/core";
import {
IconPlayerPlay,
IconPlayerStop,
IconRefresh,
} from "@tabler/icons-react";
import { listDags, getSchedulerStatus, startScheduler, stopScheduler } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import type { DagSummary, SchedulerStatus } from "../types";
export function DagList() {
const [dags, setDags] = useState<DagSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [scheduler, setScheduler] = useState<SchedulerStatus | null>(null);
const navigate = useNavigate();
const load = async () => {
setLoading(true);
setError(null);
try {
const [d, s] = await Promise.all([listDags(), getSchedulerStatus()]);
setDags(d || []);
setScheduler(s);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
const interval = setInterval(load, 10000);
return () => clearInterval(interval);
}, []);
const toggleScheduler = async () => {
if (scheduler?.running) {
await stopScheduler();
} else {
await startScheduler();
}
const s = await getSchedulerStatus();
setScheduler(s);
};
return (
<Stack gap="md">
<Group justify="space-between">
<Title order={2}>DAGs</Title>
<Group gap="xs">
<Button
size="xs"
variant="light"
leftSection={<IconRefresh size={14} />}
onClick={load}
>
Refresh
</Button>
<Button
size="xs"
variant={scheduler?.running ? "filled" : "light"}
color={scheduler?.running ? "green" : "gray"}
leftSection={
scheduler?.running ? (
<IconPlayerStop size={14} />
) : (
<IconPlayerPlay size={14} />
)
}
onClick={toggleScheduler}
>
Scheduler {scheduler?.running ? "ON" : "OFF"}
</Button>
</Group>
</Group>
{error && <Alert color="red">{error}</Alert>}
{loading && !dags.length ? (
<Loader />
) : (
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Schedule</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Tags</Table.Th>
<Table.Th>Last Status</Table.Th>
<Table.Th>Last Run</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{dags.map((d) => (
<Table.Tr
key={d.file_path}
style={{ cursor: "pointer" }}
onClick={() => navigate(`/dags/${d.name}`)}
>
<Table.Td>
<Text fw={500}>{d.name}</Text>
{d.description && (
<Text size="xs" c="dimmed" lineClamp={1}>
{d.description}
</Text>
)}
</Table.Td>
<Table.Td>
<Text size="xs" ff="monospace">
{d.schedule?.join(", ") || "-"}
</Text>
</Table.Td>
<Table.Td>
<Badge variant="light" size="xs">
{d.type || "chain"}
</Badge>
</Table.Td>
<Table.Td>
<Group gap={4}>
{d.tags?.map((t) => (
<Badge key={t} variant="dot" size="xs">
{t}
</Badge>
))}
</Group>
</Table.Td>
<Table.Td>
{d.last_run ? (
<StatusBadge status={d.last_run.Status} />
) : (
<Text size="xs" c="dimmed">
-
</Text>
)}
</Table.Td>
<Table.Td>
<Text size="xs">
{d.last_run
? new Date(d.last_run.StartedAt).toLocaleString()
: "-"}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
</Stack>
);
}
@@ -0,0 +1,105 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
Title,
Text,
Group,
Button,
Stack,
Paper,
Alert,
Loader,
} from "@mantine/core";
import { IconArrowLeft } from "@tabler/icons-react";
import { getRun } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import { StepTimeline } from "../components/StepTimeline";
import type { RunDetail as RunDetailType } from "../types";
export function RunDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [data, setData] = useState<RunDetailType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = async () => {
if (!id) return;
try {
setData(await getRun(id));
setError(null);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
// Auto-refresh while running.
const interval = setInterval(() => {
if (data?.run.Status === "running") {
load();
}
}, 2000);
return () => clearInterval(interval);
}, [id, data?.run.Status]);
if (loading) return <Loader />;
if (error) return <Alert color="red">{error}</Alert>;
if (!data) return <Text>Not found</Text>;
const { run, steps } = data;
const duration = run.FinishedAt
? `${Math.round((new Date(run.FinishedAt).getTime() - new Date(run.StartedAt).getTime()) / 1000)}s`
: "running...";
return (
<Stack gap="md">
<Group>
<Button
variant="subtle"
size="xs"
leftSection={<IconArrowLeft size={14} />}
onClick={() => navigate(`/dags/${run.DagName}`)}
>
Back to {run.DagName}
</Button>
</Group>
<Group justify="space-between">
<div>
<Title order={2}>Run {run.ID.substring(0, 16)}...</Title>
<Text size="sm" c="dimmed">
{run.DagName} &middot; {run.Trigger} &middot;{" "}
{new Date(run.StartedAt).toLocaleString()}
</Text>
</div>
<Group gap="xs">
<StatusBadge status={run.Status} />
<Text size="sm">{duration}</Text>
</Group>
</Group>
{run.Error && (
<Alert color="red" title="Error">
{run.Error}
</Alert>
)}
<Paper p="md" withBorder>
<Title order={4} mb="md">
Steps ({steps?.length || 0})
</Title>
{steps?.length ? (
<StepTimeline steps={steps} />
) : (
<Text size="sm" c="dimmed">
No steps recorded
</Text>
)}
</Paper>
</Stack>
);
}
+66
View File
@@ -0,0 +1,66 @@
export interface DagSummary {
name: string;
description?: string;
schedule?: string[];
tags?: string[];
type?: string;
file_path: string;
valid: boolean;
last_run?: DagRun;
}
export interface DagRun {
ID: string;
DagName: string;
DagPath: string;
Status: string;
Trigger: string;
StartedAt: string;
FinishedAt?: string;
Error: string;
}
export interface DagStepResult {
ID: string;
RunID: string;
StepName: string;
Status: string;
ExitCode: number;
Stdout: string;
Stderr: string;
StartedAt?: string;
FinishedAt?: string;
DurationMs: number;
Error: string;
}
export interface DagDetail {
info: DagSummary;
dag: {
Name: string;
Description: string;
Type: string;
Schedule: string[];
Steps: { Name: string; ID: string; Command: string; Script: string; Depends: string[] }[];
Env: Record<string, string>;
Tags: string[];
HandlerOn: { Failure: unknown[]; Success: unknown[] };
};
validation: {
Valid: boolean;
Errors: string[];
Warnings: string[];
Levels: string[][];
};
runs: DagRun[];
}
export interface RunDetail {
run: DagRun;
steps: DagStepResult[];
}
export interface SchedulerStatus {
running: boolean;
dags: { name: string; path: string; schedule: string; next_run: string }[];
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5175,
proxy: {
"/api": "http://localhost:8090",
},
},
build: {
outDir: "dist",
},
});
+48
View File
@@ -0,0 +1,48 @@
module dag-engine
go 1.25.0
require (
fn-registry v0.0.0-00010101000000-000000000000
github.com/mattn/go-sqlite3 v1.14.37
)
require (
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apache/arrow-go/v18 v18.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/flatbuffers v25.1.24+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/marcboeker/go-duckdb v1.8.5 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace fn-registry => /home/lucas/fn_registry
+168
View File
@@ -0,0 +1,168 @@
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y=
github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0=
github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o=
github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0=
github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"context"
"encoding/json"
"net/http"
)
func handleListDags(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dags, err := executor.ListDAGs()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, dags)
}
}
func handleGetDag(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
info, dag, validation, err := executor.GetDAG(name)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
// Get recent runs.
runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0)
resp := map[string]interface{}{
"info": info,
"dag": dag,
"validation": validation,
"runs": runs,
}
writeJSON(w, http.StatusOK, resp)
}
}
func handleRunDag(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
info, _, _, err := executor.GetDAG(name)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
// Execute asynchronously.
go func() {
ctx := context.Background()
executor.ExecuteDAG(ctx, info.FilePath, "api")
}()
// Return run acknowledgment.
writeJSON(w, http.StatusAccepted, map[string]string{
"status": "accepted",
"dag": name,
"message": "DAG execution started",
})
}
}
// --- JSON helpers ---
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"net/http"
"strconv"
)
func handleListRuns(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dagName := r.URL.Query().Get("dag")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit <= 0 || limit > 100 {
limit = 20
}
if offset < 0 {
offset = 0
}
runs, total, err := executor.store.ListRuns(dagName, limit, offset)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"runs": runs,
"total": total,
"limit": limit,
"offset": offset,
})
}
}
func handleGetRun(executor *Executor) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
run, err := executor.store.GetRun(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if run == nil {
writeError(w, http.StatusNotFound, "run not found")
return
}
steps, err := executor.store.ListStepResults(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"run": run,
"steps": steps,
})
}
}
+27
View File
@@ -0,0 +1,27 @@
package main
import "net/http"
func handleSchedulerStart(scheduler *Scheduler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := scheduler.Start(); err != nil {
writeError(w, http.StatusConflict, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "started"})
}
}
func handleSchedulerStop(scheduler *Scheduler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scheduler.Stop()
writeJSON(w, http.StatusOK, map[string]string{"status": "stopped"})
}
}
func handleSchedulerStatus(scheduler *Scheduler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
status := scheduler.Status()
writeJSON(w, http.StatusOK, status)
}
}
+336
View File
@@ -0,0 +1,336 @@
package main
import (
"context"
"embed"
"flag"
"fmt"
iofs "io/fs"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"text/tabwriter"
"time"
"fn-registry/functions/core"
"dag-engine/store"
)
//go:embed all:frontend/dist
var frontendDist embed.FS
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
cmd := os.Args[1]
args := os.Args[2:]
switch cmd {
case "run":
cmdRun(args)
case "list":
cmdList(args)
case "status":
cmdStatus(args)
case "validate":
cmdValidate(args)
case "server":
cmdServer(args)
case "help", "-h", "--help":
printUsage()
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println(`dag-engine — DAG workflow executor
Usage:
dag-engine <command> [options]
Commands:
run <path.yaml> Execute a DAG and show results
list [dir] List DAGs with schedule and last status
status [dag_name] Show execution history
validate <path.yaml> Parse and validate without executing
server Start HTTP server with web frontend
Server options:
--port <port> HTTP port (default: 8090)
--dags-dir <dir> DAGs directory (default: ~/dagu/dags)
--db <path> SQLite database path (default: dag_engine.db)
--scheduler Auto-start cron scheduler`)
}
// --- CLI Commands ---
func cmdRun(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: dag-engine run <path.yaml>")
os.Exit(1)
}
dagPath := args[0]
cfg := DefaultConfig()
// Parse optional flags after the path.
fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
fs.Parse(args[1:])
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
executor := NewExecutor(db, filepath.Dir(dagPath))
fmt.Printf("Executing %s...\n", dagPath)
ctx := context.Background()
runID, err := executor.ExecuteDAG(ctx, dagPath, "manual")
// Print results.
if runID != "" {
run, _ := db.GetRun(runID)
steps, _ := db.ListStepResults(runID)
if run != nil {
fmt.Println()
for _, s := range steps {
icon := " "
switch s.Status {
case "success":
icon = "OK"
case "failed":
icon = "!!"
case "skipped":
icon = "--"
case "running":
icon = ".."
}
fmt.Printf("[%s] %s (%dms)\n", icon, s.StepName, s.DurationMs)
if s.Status == "failed" && s.Stderr != "" {
for _, line := range strings.Split(strings.TrimSpace(s.Stderr), "\n") {
fmt.Printf(" %s\n", line)
}
}
}
fmt.Println()
dur := ""
if run.FinishedAt != nil {
dur = fmt.Sprintf(" (%s)", run.FinishedAt.Sub(run.StartedAt).Round(time.Millisecond))
}
fmt.Printf("Run %s: %s%s\n", runID, strings.ToUpper(run.Status), dur)
}
}
if err != nil {
os.Exit(1)
}
}
func cmdList(args []string) {
cfg := DefaultConfig()
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
cfg.DagsDir = args[0]
args = args[1:]
}
fs := flag.NewFlagSet("list", flag.ExitOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
fs.Parse(args)
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
executor := NewExecutor(db, cfg.DagsDir)
dags, err := executor.ListDAGs()
if err != nil {
log.Fatalf("list dags: %v", err)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tSCHEDULE\tTYPE\tTAGS\tLAST STATUS\tLAST RUN")
for _, d := range dags {
sched := strings.Join(d.Schedule, ", ")
tags := strings.Join(d.Tags, ", ")
lastStatus := "-"
lastRun := "-"
if d.LastRun != nil {
lastStatus = d.LastRun.Status
lastRun = d.LastRun.StartedAt.Format("2006-01-02 15:04")
}
typ := d.Type
if typ == "" {
typ = "chain"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", d.Name, sched, typ, tags, lastStatus, lastRun)
}
w.Flush()
}
func cmdStatus(args []string) {
cfg := DefaultConfig()
fs := flag.NewFlagSet("status", flag.ExitOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
limit := fs.Int("limit", 10, "number of runs to show")
fs.Parse(args)
dagName := ""
if fs.NArg() > 0 {
dagName = fs.Arg(0)
}
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
runs, total, err := db.ListRuns(dagName, *limit, 0)
if err != nil {
log.Fatalf("list runs: %v", err)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Showing %d of %d runs", len(runs), total)
if dagName != "" {
fmt.Fprintf(w, " for %s", dagName)
}
fmt.Fprintln(w)
fmt.Fprintln(w, "RUN_ID\tDAG\tSTATUS\tTRIGGER\tSTARTED\tDURATION")
for _, r := range runs {
dur := "-"
if r.FinishedAt != nil {
dur = r.FinishedAt.Sub(r.StartedAt).Round(time.Millisecond).String()
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
r.ID, r.DagName, r.Status, r.Trigger,
r.StartedAt.Format("2006-01-02 15:04:05"), dur)
}
w.Flush()
}
func cmdValidate(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: dag-engine validate <path.yaml>")
os.Exit(1)
}
data, err := os.ReadFile(args[0])
if err != nil {
log.Fatalf("read: %v", err)
}
dag, err := core.DagParse(data)
if err != nil {
log.Fatalf("parse error: %v", err)
}
result := core.DagValidate(dag)
fmt.Printf("DAG: %s\n", dag.Name)
fmt.Printf("Steps: %d\n", len(dag.Steps))
fmt.Printf("Schedule: %v\n", dag.Schedule)
fmt.Printf("Type: %s\n", dag.Type)
if result.Valid {
fmt.Println("Validation: PASS")
for i, level := range result.Levels {
fmt.Printf(" Level %d: %v\n", i, level)
}
} else {
fmt.Println("Validation: FAIL")
for _, e := range result.Errors {
fmt.Printf(" ERROR: %s\n", e)
}
}
for _, w := range result.Warnings {
fmt.Printf(" WARNING: %s\n", w)
}
if !result.Valid {
os.Exit(1)
}
}
// --- Server Command ---
func cmdServer(args []string) {
cfg := DefaultConfig()
fs := flag.NewFlagSet("server", flag.ExitOnError)
cfg.ParseFlags(fs, args)
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
executor := NewExecutor(db, cfg.DagsDir)
scheduler := NewScheduler(executor, cfg.DagsDir)
// Prepare frontend FS.
var feFS iofs.FS
distFS, err := iofs.Sub(frontendDist, "frontend/dist")
if err == nil {
// Check if dist has content (built frontend exists).
entries, _ := iofs.ReadDir(distFS, ".")
if len(entries) > 0 {
feFS = distFS
log.Printf("serving frontend from embedded dist/")
}
}
if feFS == nil {
log.Printf("no frontend build found, API-only mode")
}
mux := http.NewServeMux()
RegisterAPI(mux, executor, scheduler, feFS)
handler := corsMiddleware(loggingMiddleware(mux))
if cfg.AutoScheduler {
if err := scheduler.Start(); err != nil {
log.Printf("scheduler start: %v", err)
}
}
addr := fmt.Sprintf(":%d", cfg.Port)
log.Printf("dag-engine server starting on http://0.0.0.0%s", addr)
log.Printf("dags dir: %s", cfg.DagsDir)
log.Printf("database: %s", cfg.DBPath)
srv := &http.Server{Addr: addr, Handler: handler}
// Graceful shutdown.
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("shutting down...")
scheduler.Stop()
srv.Shutdown(context.Background())
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server: %v", err)
}
}
+30
View File
@@ -0,0 +1,30 @@
package main
import (
"log"
"net/http"
"time"
)
// corsMiddleware adds permissive CORS headers for development.
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// loggingMiddleware logs each HTTP request with method, path and duration.
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond))
})
}
+188
View File
@@ -0,0 +1,188 @@
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"fn-registry/functions/core"
"fn-registry/functions/infra"
)
// ScheduledDAG represents a DAG with a parsed cron schedule.
type ScheduledDAG struct {
Name string `json:"name"`
Path string `json:"path"`
Schedule string `json:"schedule"`
NextRun time.Time `json:"next_run"`
}
// Scheduler manages cron-triggered DAG execution.
type Scheduler struct {
mu sync.Mutex
running bool
cancel context.CancelFunc
dagsDir string
executor *Executor
dags []ScheduledDAG
}
// NewScheduler creates a new scheduler.
func NewScheduler(executor *Executor, dagsDir string) *Scheduler {
return &Scheduler{
executor: executor,
dagsDir: dagsDir,
}
}
// Start scans for DAGs with schedules and starts cron tickers for each.
func (s *Scheduler) Start() error {
s.mu.Lock()
if s.running {
s.mu.Unlock()
return fmt.Errorf("scheduler already running")
}
ctx, cancel := context.WithCancel(context.Background())
s.cancel = cancel
s.running = true
s.mu.Unlock()
scheduled, err := s.scanDAGs()
if err != nil {
s.mu.Lock()
s.running = false
s.mu.Unlock()
cancel()
return err
}
s.mu.Lock()
s.dags = scheduled
s.mu.Unlock()
log.Printf("[scheduler] started with %d DAGs", len(scheduled))
for _, dag := range scheduled {
dag := dag
go s.runTicker(ctx, dag)
}
return nil
}
// Stop cancels all tickers and stops the scheduler.
func (s *Scheduler) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
if !s.running {
return
}
s.cancel()
s.running = false
s.dags = nil
log.Printf("[scheduler] stopped")
}
// IsRunning returns true if the scheduler is active.
func (s *Scheduler) IsRunning() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.running
}
// Status returns the list of scheduled DAGs with their next run time.
type SchedulerStatus struct {
Running bool `json:"running"`
DAGs []ScheduledDAG `json:"dags"`
}
func (s *Scheduler) Status() SchedulerStatus {
s.mu.Lock()
defer s.mu.Unlock()
return SchedulerStatus{
Running: s.running,
DAGs: s.dags,
}
}
// scanDAGs reads the dags directory and returns DAGs that have cron schedules.
func (s *Scheduler) scanDAGs() ([]ScheduledDAG, error) {
entries, err := os.ReadDir(s.dagsDir)
if err != nil {
return nil, err
}
var scheduled []ScheduledDAG
for _, entry := range entries {
ext := filepath.Ext(entry.Name())
if ext != ".yaml" && ext != ".yml" {
continue
}
path := filepath.Join(s.dagsDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
continue
}
dag, err := core.DagParse(data)
if err != nil {
continue
}
for _, expr := range dag.Schedule {
sched, err := core.ParseCronExpr(strings.TrimSpace(expr))
if err != nil {
log.Printf("[scheduler] invalid cron %q in %s: %v", expr, dag.Name, err)
continue
}
next := core.NextCronTime(sched, time.Now())
scheduled = append(scheduled, ScheduledDAG{
Name: dag.Name,
Path: path,
Schedule: expr,
NextRun: next,
})
}
}
return scheduled, nil
}
// runTicker starts a cron ticker for a single DAG schedule.
func (s *Scheduler) runTicker(ctx context.Context, dag ScheduledDAG) {
sched, err := core.ParseCronExpr(strings.TrimSpace(dag.Schedule))
if err != nil {
return
}
// Convert core.CronSchedule to infra.CronTickerSchedule.
tickerSched := infra.CronTickerSchedule{
Minute: sched.Minute,
Hour: sched.Hour,
DayOfMonth: sched.DayOfMonth,
Month: sched.Month,
DayOfWeek: sched.DayOfWeek,
}
ch := infra.CronTicker(tickerSched, ctx)
log.Printf("[scheduler] ticker started for %s (%s), next: %s", dag.Name, dag.Schedule, dag.NextRun.Format(time.RFC3339))
for t := range ch {
log.Printf("[scheduler] triggered %s at %s", dag.Name, t.Format(time.RFC3339))
go func() {
runID, err := s.executor.ExecuteDAG(ctx, dag.Path, "cron")
if err != nil {
log.Printf("[scheduler] %s failed: %v (run: %s)", dag.Name, err, runID)
} else {
log.Printf("[scheduler] %s completed (run: %s)", dag.Name, runID)
}
}()
}
}
@@ -0,0 +1,29 @@
CREATE TABLE IF NOT EXISTS dag_runs (
id TEXT PRIMARY KEY,
dag_name TEXT NOT NULL,
dag_path TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','success','failed','cancelled')),
trigger TEXT NOT NULL DEFAULT 'manual' CHECK(trigger IN ('manual','cron','api')),
started_at TEXT NOT NULL,
finished_at TEXT,
error TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS dag_step_results (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL REFERENCES dag_runs(id) ON DELETE CASCADE,
step_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','success','failed','skipped')),
exit_code INTEGER NOT NULL DEFAULT -1,
stdout TEXT NOT NULL DEFAULT '',
stderr TEXT NOT NULL DEFAULT '',
started_at TEXT,
finished_at TEXT,
duration_ms INTEGER NOT NULL DEFAULT 0,
error TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_runs_dag_name ON dag_runs(dag_name);
CREATE INDEX IF NOT EXISTS idx_runs_status ON dag_runs(status);
CREATE INDEX IF NOT EXISTS idx_runs_started ON dag_runs(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_step_results_run ON dag_step_results(run_id);
+231
View File
@@ -0,0 +1,231 @@
package store
import (
"database/sql"
_ "embed"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migrations/001_init.sql
var migrationSQL string
// DB wraps a SQLite connection for DAG run persistence.
type DB struct {
conn *sql.DB
path string
}
// Open opens or creates a DAG engine database at the given path.
func Open(path string) (*DB, error) {
conn, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on")
if err != nil {
return nil, fmt.Errorf("store: open %s: %w", path, err)
}
if _, err := conn.Exec(migrationSQL); err != nil {
conn.Close()
return nil, fmt.Errorf("store: migrate: %w", err)
}
return &DB{conn: conn, path: path}, nil
}
// Close closes the database connection.
func (db *DB) Close() error {
return db.conn.Close()
}
// --- DagRun CRUD ---
// DagRun mirrors infra.DagRun for the store layer.
type DagRun struct {
ID string
DagName string
DagPath string
Status string
Trigger string
StartedAt time.Time
FinishedAt *time.Time
Error string
}
// CreateRun inserts a new run record.
func (db *DB) CreateRun(run *DagRun) error {
_, err := db.conn.Exec(
`INSERT INTO dag_runs (id, dag_name, dag_path, status, trigger, started_at, error)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
run.ID, run.DagName, run.DagPath, run.Status, run.Trigger,
run.StartedAt.Format(time.RFC3339), run.Error,
)
return err
}
// UpdateRunStatus updates a run's status and optionally its finished_at and error.
func (db *DB) UpdateRunStatus(id, status string, finishedAt *time.Time, errMsg string) error {
var fin *string
if finishedAt != nil {
s := finishedAt.Format(time.RFC3339)
fin = &s
}
_, err := db.conn.Exec(
`UPDATE dag_runs SET status=?, finished_at=?, error=? WHERE id=?`,
status, fin, errMsg, id,
)
return err
}
// GetRun retrieves a single run by ID.
func (db *DB) GetRun(id string) (*DagRun, error) {
row := db.conn.QueryRow(
`SELECT id, dag_name, dag_path, status, trigger, started_at, finished_at, error
FROM dag_runs WHERE id=?`, id,
)
return scanRun(row)
}
// ListRuns returns runs, newest first, with optional dag name filter.
func (db *DB) ListRuns(dagName string, limit, offset int) ([]DagRun, int, error) {
var total int
var args []interface{}
where := ""
if dagName != "" {
where = " WHERE dag_name=?"
args = append(args, dagName)
}
err := db.conn.QueryRow("SELECT COUNT(*) FROM dag_runs"+where, args...).Scan(&total)
if err != nil {
return nil, 0, err
}
query := "SELECT id, dag_name, dag_path, status, trigger, started_at, finished_at, error FROM dag_runs" +
where + " ORDER BY started_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.conn.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var runs []DagRun
for rows.Next() {
r, err := scanRunRows(rows)
if err != nil {
return nil, 0, err
}
runs = append(runs, *r)
}
return runs, total, rows.Err()
}
// --- DagStepResult CRUD ---
// DagStepResult mirrors infra.DagStepResult for the store layer.
type DagStepResult struct {
ID string
RunID string
StepName string
Status string
ExitCode int
Stdout string
Stderr string
StartedAt *time.Time
FinishedAt *time.Time
DurationMs int64
Error string
}
// InsertStepResult inserts a new step result.
func (db *DB) InsertStepResult(r *DagStepResult) error {
var startedAt, finishedAt *string
if r.StartedAt != nil {
s := r.StartedAt.Format(time.RFC3339)
startedAt = &s
}
if r.FinishedAt != nil {
s := r.FinishedAt.Format(time.RFC3339)
finishedAt = &s
}
_, err := db.conn.Exec(
`INSERT INTO dag_step_results (id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.RunID, r.StepName, r.Status, r.ExitCode, r.Stdout, r.Stderr,
startedAt, finishedAt, r.DurationMs, r.Error,
)
return err
}
// UpdateStepResult updates a step result by ID.
func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr string, finishedAt *time.Time, durationMs int64, errMsg string) error {
var fin *string
if finishedAt != nil {
s := finishedAt.Format(time.RFC3339)
fin = &s
}
_, err := db.conn.Exec(
`UPDATE dag_step_results SET status=?, exit_code=?, stdout=?, stderr=?, finished_at=?, duration_ms=?, error=? WHERE id=?`,
status, exitCode, stdout, stderr, fin, durationMs, errMsg, id,
)
return err
}
// ListStepResults returns all step results for a given run.
func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
rows, err := db.conn.Query(
`SELECT id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var results []DagStepResult
for rows.Next() {
var r DagStepResult
var startedAt, finishedAt sql.NullString
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode,
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
return nil, err
}
if startedAt.Valid {
t, _ := time.Parse(time.RFC3339, startedAt.String)
r.StartedAt = &t
}
if finishedAt.Valid {
t, _ := time.Parse(time.RFC3339, finishedAt.String)
r.FinishedAt = &t
}
results = append(results, r)
}
return results, rows.Err()
}
// --- scan helpers ---
type scanner interface {
Scan(dest ...interface{}) error
}
func scanRun(s scanner) (*DagRun, error) {
var r DagRun
var startedAt string
var finishedAt sql.NullString
if err := s.Scan(&r.ID, &r.DagName, &r.DagPath, &r.Status, &r.Trigger, &startedAt, &finishedAt, &r.Error); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
r.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
if finishedAt.Valid {
t, _ := time.Parse(time.RFC3339, finishedAt.String)
r.FinishedAt = &t
}
return &r, nil
}
func scanRunRows(rows *sql.Rows) (*DagRun, error) {
return scanRun(rows)
}
-5
View File
@@ -1,5 +0,0 @@
build/
*.exe
*.dll
*.so
*.dylib
-19
View File
@@ -1,19 +0,0 @@
.PHONY: run build clean install tidy help
run: ## Ejecuta la TUI
go run .
build: ## Compila el binario
go build -trimpath -ldflags='-s -w' -o build/docker-tui .
clean: ## Limpia artefactos
rm -rf build/
install: build ## Instala en ~/.local/bin
cp build/docker-tui ~/.local/bin/docker-tui
tidy: ## go mod tidy
go mod tidy
help: ## Muestra esta ayuda
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
-175
View File
@@ -1,175 +0,0 @@
package app
import (
"docker-tui/views"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lucasdataproyects/devfactory/tui"
)
type View int
const (
ViewContainers View = iota
ViewImages
ViewVolumes
ViewNetworks
ViewCompose
)
var tabNames = []string{"Containers", "Images", "Volumes", "Networks", "Compose"}
type Model struct {
tui.BaseModel
activeTab int
containers views.ContainersModel
images views.ImagesModel
volumes views.VolumesModel
networks views.NetworksModel
compose views.ComposeModel
ready bool
}
func New() Model {
styles := tui.DefaultStyles()
return Model{
BaseModel: tui.NewBaseModel().WithStyles(styles),
containers: views.NewContainersModel(styles),
images: views.NewImagesModel(styles),
volumes: views.NewVolumesModel(styles),
networks: views.NewNetworksModel(styles),
compose: views.NewComposeModel(styles),
}
}
func (m Model) Init() tea.Cmd {
return m.containers.Init()
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case views.KeyQuit:
return m, tea.Quit
case "q", "0", "esc":
updated, atBase := m.handleBack()
if atBase {
return updated, tea.Quit
}
return updated, nil
case views.KeyTab:
m.activeTab = (m.activeTab + 1) % len(tabNames)
return m, m.initActiveView()
case "shift+tab":
m.activeTab = (m.activeTab - 1 + len(tabNames)) % len(tabNames)
return m, m.initActiveView()
}
case tea.WindowSizeMsg:
m.HandleWindowSize(msg)
m.ready = true
}
var cmd tea.Cmd
switch View(m.activeTab) {
case ViewContainers:
m.containers, cmd = m.containers.Update(msg)
case ViewImages:
m.images, cmd = m.images.Update(msg)
case ViewVolumes:
m.volumes, cmd = m.volumes.Update(msg)
case ViewNetworks:
m.networks, cmd = m.networks.Update(msg)
case ViewCompose:
m.compose, cmd = m.compose.Update(msg)
}
return m, cmd
}
func (m Model) View() string {
if !m.ready {
return "Loading..."
}
// Tab bar
tabs := m.renderTabs()
// Active view content
var content string
switch View(m.activeTab) {
case ViewContainers:
content = m.containers.View()
case ViewImages:
content = m.images.View()
case ViewVolumes:
content = m.volumes.View()
case ViewNetworks:
content = m.networks.View()
case ViewCompose:
content = m.compose.View()
}
// Status bar
status := m.Styles.StatusBar.Render(" Tab: switch view │ Ctrl+C: quit │ Enter: action │ r: refresh")
return lipgloss.JoinVertical(lipgloss.Left,
tabs,
"",
content,
"",
status,
)
}
func (m Model) renderTabs() string {
var tabs []string
for i, name := range tabNames {
if i == m.activeTab {
tabs = append(tabs, m.Styles.Selected.Render(" "+name+" "))
} else {
tabs = append(tabs, m.Styles.Muted.Render(" "+name+" "))
}
}
row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
return m.Styles.Header.Render("Docker TUI") + " " + row
}
// handleBack asks the active view to go back one level.
// Returns the updated model and true if the view was already at base level (app should quit).
func (m Model) handleBack() (Model, bool) {
switch View(m.activeTab) {
case ViewContainers:
atBase := m.containers.HandleBack()
return m, atBase
case ViewImages:
atBase := m.images.HandleBack()
return m, atBase
case ViewVolumes:
atBase := m.volumes.HandleBack()
return m, atBase
case ViewNetworks:
atBase := m.networks.HandleBack()
return m, atBase
case ViewCompose:
atBase := m.compose.HandleBack()
return m, atBase
}
return m, true
}
func (m Model) initActiveView() tea.Cmd {
switch View(m.activeTab) {
case ViewContainers:
return m.containers.Init()
case ViewImages:
return m.images.Init()
case ViewVolumes:
return m.volumes.Init()
case ViewNetworks:
return m.networks.Init()
case ViewCompose:
return m.compose.Init()
}
return nil
}
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
echo "==> Tidying modules..."
go mod tidy
echo "==> Building docker-tui..."
mkdir -p build
go build -trimpath -ldflags='-s -w' -o build/docker-tui .
echo "==> Done: build/docker-tui ($(du -h build/docker-tui | cut -f1))"
echo " Run with: ./build/docker-tui"
-15
View File
@@ -1,15 +0,0 @@
package config
// Config holds Docker TUI configuration.
type Config struct {
ComposeFile string
RefreshInterval int // seconds, 0 = manual
}
// Default returns sensible defaults.
func Default() Config {
return Config{
ComposeFile: "docker-compose.yml",
RefreshInterval: 0,
}
}
-43
View File
@@ -1,43 +0,0 @@
module docker-tui
go 1.22.2
require (
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/lucasdataproyects/devfactory v0.0.0
)
require (
github.com/apache/arrow/go/v14 v14.0.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v0.18.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/marcboeker/go-duckdb v1.6.5 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
)
replace github.com/lucasdataproyects/devfactory => /home/lucas/.local_agentes/backend
-84
View File
@@ -1,84 +0,0 @@
github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO5IONw=
github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/marcboeker/go-duckdb v1.6.5 h1:XCfR1JVZxsemcSPxRQKK0R0ESfgRMHTEqh3Y+dv40SI=
github.com/marcboeker/go-duckdb v1.6.5/go.mod h1:WtWeqqhZoTke/Nbd7V9lnBx7I2/A/q0SAq/urGzPCMs=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o=
gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-6
View File
@@ -1,6 +0,0 @@
go 1.22.2
use (
.
/home/lucas/.local_agentes/backend
)
-40
View File
@@ -1,40 +0,0 @@
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

Some files were not shown because too many files have changed in this diff Show More