From e28d95ce6fbb93c9b19b49537294a303974756ef Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 19:13:53 +0100 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20operations.db=20en=20ubicaci=C3=B3n?= =?UTF-8?q?=20correcta=20seg=C3=BAn=20reglas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elimina operations.db de docker_tui (no lo usaba) y corrige pipeline_launcher para que resuelva operations.db en su propia carpeta (apps/pipeline_launcher/) en vez de la raíz del registro. --- apps/docker_tui/operations.db | Bin 53248 -> 0 bytes apps/pipeline_launcher/config/config.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 apps/docker_tui/operations.db diff --git a/apps/docker_tui/operations.db b/apps/docker_tui/operations.db deleted file mode 100644 index e730dc7b152e426f5b4683121f31deec608f9925..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 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 Date: Sat, 28 Mar 2026 19:14:49 +0100 Subject: [PATCH 2/6] docs: reorganizar CLAUDE.md y extraer reglas a .claude/rules/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifica CLAUDE.md eliminando contenido redundante y extrae las reglas operativas a archivos independientes en .claude/rules/ con un INDEX.md. Cada regla es atómica y referenciable. --- .claude/CLAUDE.md | 669 ++++----------------------- .claude/rules/INDEX.md | 15 + .claude/rules/assertions.md | 3 + .claude/rules/db_locations.md | 5 + .claude/rules/go_packages.md | 1 + .claude/rules/ids_naming.md | 3 + .claude/rules/proposals.md | 3 + .claude/rules/purity.md | 5 + .claude/rules/stubs.md | 1 + .claude/rules/tag_launcher.md | 5 + .claude/rules/types_in_signatures.md | 3 + 11 files changed, 141 insertions(+), 572 deletions(-) create mode 100644 .claude/rules/INDEX.md create mode 100644 .claude/rules/assertions.md create mode 100644 .claude/rules/db_locations.md create mode 100644 .claude/rules/go_packages.md create mode 100644 .claude/rules/ids_naming.md create mode 100644 .claude/rules/proposals.md create mode 100644 .claude/rules/purity.md create mode 100644 .claude/rules/stubs.md create mode 100644 .claude/rules/tag_launcher.md create mode 100644 .claude/rules/types_in_signatures.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6c837914..7d0c29f7 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,394 +2,57 @@ Registry personal de codigo reutilizable con busqueda FTS. Diseñado para composicion funcional y agentes. -El sistema tiene **dos bases de datos SQLite**: -- **registry.db** — conocimiento estatico: que funciones y tipos existen, como se componen, y que mejoras se proponen. Vive en la raiz del repositorio. Regenerable con `fn index` (excepto proposals). -- **operations.db** — conocimiento dinamico por proyecto: que entities existen, como se relacionan, que ejecuciones se han hecho, y que calidad tienen los datos. Vive en cada proyecto que usa el registry. +**Dos bases de datos SQLite:** +- **registry.db** (raiz) — funciones, tipos, proposals. Regenerable con `fn index` (excepto proposals). +- **operations.db** (por app en `apps/*/`) — entities, relations, executions, assertions. Datos vivos. ---- - -## Arquitectura: dos BDs, un bucle - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ registry.db (central) │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ functions │ │ types │ │proposals │ │ -│ │ (120+) │ │ (31+) │ │(auto/man)│ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ ↑ busca funciones ↑ crea proposals │ -└───────┼────────────────────────────┼────────────────────────────────┘ - │ │ -┌───────┼────────────────────────────┼────────────────────────────────┐ -│ ↓ usa en relaciones │ reactive loop │ -│ operations.db (por proyecto) │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ -│ │ entities │→ │relations │ │executions│→ │assertions │ │ -│ │ │ │ │ │ │ │ → assertion_results│ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ │ -│ │relation_inputs│ │types_snapshot│ │ -│ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -**Bucle autonomo:** EJECUTAR → EVALUAR → REACCIONAR → PROPONER -1. Un pipeline se ejecuta → se registra en `executions` -2. Las `assertions` activas de la entity se evaluan automaticamente -3. Si critical falla → entity pasa a `corrupted` + se crea `proposal` en registry -4. Si warning falla → entity pasa a `stale` -5. El humano revisa proposals y decide si implementar mejoras +**Reglas y convenciones:** ver `.claude/rules/INDEX.md` --- ## Explorar el registry (USAR SIEMPRE) -La BD SQLite `registry.db` es tu mapa del repositorio. Antes de escribir codigo, SIEMPRE consultala para saber que existe, evitar duplicados y descubrir funciones reutilizables. +Antes de escribir codigo, SIEMPRE consulta registry.db para evitar duplicados y descubrir funciones reutilizables. ```bash -# Buscar funciones por texto libre (FTS5) +# 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;" -# Listar todas las funciones de un dominio +# Por dominio sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;" -# Listar solo puras de un dominio +# Puras de un dominio sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;" -# Listar solo impuras -sqlite3 registry.db "SELECT id, domain, error_type FROM functions WHERE purity = 'impure' ORDER BY domain, name;" - -# Buscar tipos por dominio +# Tipos sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';" -# Ver que funciones usa un pipeline o funcion compuesta +# Dependencias sqlite3 registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE uses_functions != '[]';" -# Ver funciones que dependen de un tipo concreto -sqlite3 registry.db "SELECT id FROM functions WHERE uses_types LIKE '%ohlcv_go_finance%';" - -# Buscar por tag -sqlite3 registry.db "SELECT id, tags FROM functions WHERE tags LIKE '%generic%';" - -# Contar entradas por dominio -sqlite3 registry.db "SELECT domain, COUNT(*) FROM functions GROUP BY domain;" - -# Ver todo el schema -sqlite3 registry.db ".schema" - -# Ver proposals pendientes +# Proposals pendientes sqlite3 registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending';" -``` -La BD usa WAL mode — puedes leerla mientras se escribe sin bloqueos. +# Schema completo +sqlite3 registry.db ".schema" +``` --- -## Tablas: registry.db - -### functions (27 columnas) - -Cada funcion registrada: su firma, purity, dependencias, y metadata. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | `{name}_{lang}_{domain}` | -| name | TEXT | snake_case | -| kind | TEXT | `function` / `pipeline` / `component` | -| lang | TEXT | `go` / `python` / `typescript` / `sql` | -| domain | TEXT | `core` / `finance` / `datascience` / `cybersecurity` / ... | -| version | TEXT | semver | -| purity | TEXT | `pure` / `impure` | -| signature | TEXT | firma completa | -| description | TEXT | que hace y cuando usarla | -| tags | JSON[] | etiquetas | -| uses_functions | JSON[] | IDs de funciones que invoca | -| uses_types | JSON[] | IDs de tipos que recibe | -| returns | JSON[] | IDs de tipos que devuelve (no tipos nativos) | -| returns_optional | INT | 0/1 | -| error_type | TEXT | ID del tipo de error (obligatorio si impure) | -| imports | JSON[] | dependencias externas | -| example | TEXT | codigo de ejemplo extraido del .md | -| tested | INT | 0/1 | -| tests | JSON[] | nombres de tests | -| test_file_path | TEXT | ruta al archivo de test | -| file_path | TEXT | ruta relativa al .go/.py/.tsx | -| props | JSON[] | solo components: PropDef[] | -| emits | JSON[] | solo components: eventos emitidos | -| has_state | INT? | solo components: nullable | -| framework | TEXT | solo components: react/vue/... | -| variant | JSON[] | solo components: variantes | -| created_at, updated_at | TEXT | RFC3339 | - -### types (13 columnas) - -Tipos algebraicos: product (struct) y sum (interface/union). - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | `{name}_{lang}_{domain}` | -| name | TEXT | snake_case | -| lang | TEXT | lenguaje | -| domain | TEXT | dominio | -| version | TEXT | semver | -| algebraic | TEXT | `product` / `sum` | -| definition | TEXT | codigo fuente del tipo | -| description | TEXT | descripcion | -| tags | JSON[] | etiquetas | -| uses_types | JSON[] | IDs de tipos que compone (sin auto-ref) | -| file_path | TEXT | ruta relativa | -| created_at, updated_at | TEXT | RFC3339 | - -### proposals (11 columnas) - -Mejoras propuestas al registry. Las crea el agente (reactive loop) o el humano. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | `proposal_{timestamp}` o manual | -| kind | TEXT | `new_function` / `new_type` / `improve_function` / `improve_type` / `new_pipeline` | -| target_id | TEXT | ID de la funcion/tipo a mejorar (obligatorio para improve_*) | -| title | TEXT | titulo corto | -| description | TEXT | detalle | -| evidence | JSON{} | datos que justifican la propuesta (assertion failures, metrics, etc) | -| status | TEXT | `pending` / `approved` / `rejected` / `implemented` | -| created_by | TEXT | quien creo (agente, humano, reactive_loop) | -| reviewed_by | TEXT | quien reviso | -| created_at, updated_at | TEXT | RFC3339 | - -### FTS5 (busqueda full-text) - -- `functions_fts` — indexa: id, name, description, tags, signature, domain -- `types_fts` — indexa: id, name, description, tags, domain -- `proposals_fts` — indexa: id, title, description, evidence - -Sincronizados automaticamente via triggers (INSERT/UPDATE/DELETE). - ---- - -## Tablas: operations.db - -### entities (12 columnas) - -Instancia concreta de un tipo del registry dentro de un proyecto. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | identificador unico en el proyecto | -| name | TEXT | nombre descriptivo | -| type_ref | TEXT | ID del tipo en registry (ej: `ohlcv_go_finance`) | -| status | TEXT | `active` / `stale` / `corrupted` / `archived` | -| description | TEXT | que representa esta entity | -| domain | TEXT | dominio | -| tags | JSON[] | etiquetas | -| source | TEXT | origen de los datos (obligatorio) | -| metadata | JSON{} | campos del tipo instanciados con valores reales | -| notes | TEXT | notas libres | -| created_at, updated_at | TEXT | RFC3339 | - -### relations (17 columnas) - -Conexion/transformacion entre entities. Puede ser causal (via funcion) o semantica. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | identificador | -| name | TEXT | nombre descriptivo | -| from_entity | TEXT | entity origen | -| to_entity | TEXT | entity destino (obligatorio) | -| via | TEXT | ID de funcion del registry que transforma (si vacio = semantica) | -| description | TEXT | que hace esta relacion | -| purity | TEXT | `pure` / `impure` / `` | -| direction | TEXT | `unidirectional` / `bidirectional` / `inverse` | -| weight | REAL | 0.0-1.0, importancia | -| status | TEXT | `designed` / `implemented` / `tested` / `running` / `deprecated` | -| started_at, ended_at | TEXT | ciclo de vida | -| order | INT | para secuencias | -| tags | JSON[] | etiquetas | -| notes | TEXT | notas | -| created_at, updated_at | TEXT | RFC3339 | - -### relation_inputs (5 columnas) - -Multi-input para relaciones N→1 (joins, merges, agregaciones). - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | | -| relation_id | TEXT FK | referencia a relations.id (CASCADE delete) | -| entity_id | TEXT FK | referencia a entities.id | -| role | TEXT | rol semantico del input (ej: "left", "right", "config") | -| order | INT | orden de procesamiento | - -### types_snapshot (7 columnas) - -Copia inmutable de un tipo del registry en el momento de uso. Hace operations.db autonoma. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | mismo ID que en registry.types | -| version | TEXT | version capturada | -| lang | TEXT | lenguaje | -| algebraic | TEXT | product/sum | -| definition | TEXT | codigo fuente capturado | -| description | TEXT | descripcion capturada | -| snapped_at | TEXT | cuando se hizo el snapshot | - -### executions (12 columnas) - -Cada ejecucion de un pipeline. Memoria de comportamiento del sistema. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | `exec_{timestamp}` o manual | -| pipeline_id | TEXT | ID de funcion del registry (ej: `tick_to_ohlcv_go_finance`) | -| relation_id | TEXT | relacion asociada (opcional) | -| status | TEXT | `success` / `failure` / `partial` | -| started_at | TEXT | inicio (obligatorio) | -| ended_at | TEXT | fin (nullable si en progreso) | -| duration_ms | INT | auto-calculado si started_at y ended_at presentes | -| records_in | INT | registros de entrada (nullable) | -| records_out | INT | registros de salida (nullable) | -| error | TEXT | mensaje de error si fallo | -| metrics | JSON{} | metricas custom (ej: `{"mean_close": 42000}`) | -| created_at | TEXT | RFC3339 | - -### assertions (9 columnas) - -Regla de calidad formal sobre una entity. Evaluable automaticamente contra metadata. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | | -| entity_id | TEXT FK | referencia a entities.id | -| name | TEXT | nombre descriptivo | -| kind | TEXT | tipo libre: `range`, `null`, `statistical`, `consistency`, `freshness`, o custom | -| rule | TEXT | expresion SQL evaluable (ver motor de evaluacion abajo) | -| severity | TEXT | `critical` / `warning` / `info` | -| description | TEXT | que verifica | -| active | INT | 0/1 — solo las activas se evaluan | -| created_at | TEXT | RFC3339 | - -### assertion_results (7 columnas) - -Historial de evaluaciones de assertions. - -| Columna | Tipo | Descripcion | -|---|---|---| -| id | TEXT PK | | -| assertion_id | TEXT FK | referencia a assertions.id | -| execution_id | TEXT | referencia a executions.id (vacio si eval manual) | -| status | TEXT | `pass` / `fail` / `skip` | -| value | JSON{} | datos capturados en el momento de evaluacion | -| message | TEXT | detalle del resultado | -| evaluated_at | TEXT | RFC3339 | - -### FTS5 operations - -- `entities_fts` — indexa: id, name, description, tags, domain -- `assertions_fts` — indexa: id, name, description, rule - ---- - -## Motor de evaluacion de assertions - -Las rules se escriben como expresiones SQL. Campos sin prefijo se reescriben automaticamente a `json_extract(metadata, '$.campo')`: - -``` -close > 0 → json_extract(metadata, '$.close') > 0 -low <= close AND close <= high → json_extract(metadata, '$.low') <= ... -open IS NOT NULL → json_extract(metadata, '$.open') IS NOT NULL -``` - -Si la rule ya usa `json_extract`, se deja como esta. Palabras SQL (AND, OR, NOT, IS, NULL, BETWEEN, etc) y funciones SQLite (datetime, abs, max, min, etc) no se reescriben. - -**Kinds documentados** (puedes añadir nuevos sin tocar schema): - -| Kind | Descripcion | Ejemplo de rule | -|---|---|---| -| `range` | Valor dentro de rango | `close BETWEEN 0 AND 1000000` | -| `null` | Campo no nulo | `open IS NOT NULL` | -| `consistency` | Relacion entre campos | `low <= close AND close <= high` | -| `freshness` | Datos recientes | `json_extract(metadata, '$.ts') > datetime('now', '-1 hour')` | -| `statistical` | Desviacion estadistica | (evaluar externamente, registrar manual) | - -**Dos modos:** -- **Auto**: `fn ops assertion eval --entity-id X` ejecuta rules SQL contra `entities.metadata` -- **Manual**: `fn ops assertion result add` registra resultados de assertions que el sistema no puede evaluar - -**Bucle reactivo** (con `--react`): -- `fn ops assertion eval --entity-id X --react` evalua Y reacciona: - - Critical fail → entity.status = `corrupted` + auto-crea proposal en registry.db - - Warning fail → entity.status = `stale` (solo si era `active`) - - Info fail → sin cambio - ---- - -## Sistema de migraciones - -Ambas BDs usan un sistema de migraciones con `embed.FS`: - -``` -registry/migrations/ - 001_init.sql # functions + types + FTS - 002_proposals.sql # proposals + FTS - -fn_operations/migrations/ - 001_init.sql # entities + relations + relation_inputs + types_snapshot + FTS - 002_executions_assertions.sql # executions + assertions + assertion_results + FTS -``` - -- Tabla `schema_migrations` en cada BD rastrea versiones aplicadas -- `CREATE TABLE IF NOT EXISTS` + transacciones por migracion = idempotente -- Al hacer `Open()` se aplican automaticamente las migraciones pendientes -- Para añadir una nueva migracion: crear `NNN_nombre.sql` en la carpeta correspondiente - ---- - -## Estructura del repositorio +## Estructura ``` fn-registry/ - functions/ # Codigo y docs de funciones - core/ # Utilidades genericas (filter, map, pipeline, retry...) - finance/ # Indicadores, riesgo, IO de mercado - datascience/ # Estadistica, DSP, IO de datos - cybersecurity/ # Crypto, analisis de red, IO de seguridad - pipelines/ # Composiciones de funciones, siempre impuras - components/ # Componentes React (.tsx) - types/ # Tipos algebraicos (product y sum) - core/ # Result, Option, Pair, Error - finance/ # OHLCV, Tick, BollingerResult, DrawdownResult - datascience/ # OutlierResult - cybersecurity/ # CIDRBlock, ThreatResult, PortResult + functions/{domain}/ # .go + .md por funcion (core, finance, datascience, cybersecurity) + functions/pipelines/ # Composiciones, siempre impuras + functions/components/ # React (.tsx) + types/{domain}/ # .go + .md por tipo registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones - models.go # Function, Type, Proposal structs + enums (Kind, Purity, Algebraic, ProposalKind, ProposalStatus) - db.go # Open/Close/Drop + WAL + migraciones - store.go # CRUD: Insert/Get/Update/Delete/List/Search para functions, types, proposals - validate.go # ValidateFunction, ValidateType, ValidateProposal - parser.go # ParseFunctionMD, ParseTypeMD (YAML frontmatter) - indexer.go # Index() — two-pass: parse → validate refs → insert - migrate.go # Motor de migraciones (embed.FS) - migrations/ # Archivos .sql numerados - fn_operations/ # Paquete Go: operations database (libreria, NO apps) - models.go # Entity, Relation, RelationInput, TypeSnapshot, Execution, Assertion, AssertionResult + enums - db.go # Open/Close/Drop/Conn - store.go # CRUD para todas las tablas - validate.go # ValidateEntity, ValidateRelation, ValidateExecution, ValidateAssertion, DetectCycle - operations.go # Alto nivel: InsertEntityWithSnapshot, InsertRelationSafe, React, ExecuteAndReact - eval.go # Motor de evaluacion: rewriteRule, EvalAssertion, EvalEntityAssertions - migrate.go # Motor de migraciones - migrations/ # Archivos .sql numerados - apps/ # Aplicaciones ejecutables (TUIs, CLIs) — modulos Go independientes - docker_tui/ # TUI fullscreen para gestionar Docker - pipeline_launcher/ # TUI para lanzar pipelines y registrar ejecuciones - cmd/fn/ # CLI - main.go # Subcomandos: index, search, list, show, add, ops, proposal - ops.go # fn ops: entity, relation, graph, snapshot, execution, assertion - proposal.go # fn proposal: add, list, show, update - docs/ # Specs de diseño (fuente de verdad del schema) + fn_operations/ # Paquete Go: operations database (libreria) + apps/ # Apps ejecutables (TUIs, CLIs) — modulos Go independientes, cada una con su operations.db + cmd/fn/ # CLI principal + docs/ # Specs de diseño docs/templates/ # Plantillas de frontmatter - registry.db # Indice SQLite FTS5+WAL (regenerable con fn index, excepto proposals) ``` --- @@ -397,200 +60,107 @@ fn-registry/ ## Build ```bash -CGO_ENABLED=1 go build -tags fts5 ./... -CGO_ENABLED=1 go test -tags fts5 ./... CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/ +CGO_ENABLED=1 go test -tags fts5 ./... ``` --- -## CLI completo (cmd/fn) - -### Registry +## CLI ```bash -fn index # Regenera registry.db desde los .md -fn search "texto" # Busqueda FTS en functions + types +# Registry +fn index # Regenera registry.db +fn search "texto" # FTS en functions + types fn search -k function -p pure -d core "slice" -fn list # Lista todo -fn list -d finance -k function # Lista por dominio y kind -fn show filter_slice_go_core # Muestra entrada completa -fn add -k function # Muestra template para copiar -``` +fn list [-d domain] [-k kind] +fn show +fn add -k function # Template -### Proposals - -```bash -fn proposal add --kind new_function --title "..." --created-by agent [--target-id ] [--evidence '{}'] [--description "..."] +# Proposals +fn proposal add --kind new_function --title "..." --created-by agent [--target-id ] fn proposal list [-k kind] [-s status] fn proposal show fn proposal update --status approved [--reviewed-by lucas] -``` -### Operations - -```bash -fn ops init [path] # Crea operations.db en el directorio -fn ops help # Ayuda - -# Entities -fn ops entity add --id --name --type-ref --source [--metadata '{}'] [--domain d] [--tags t1,t2] -fn ops entity list [--domain d] [--status s] -fn ops entity show -fn ops entity delete - -# Relations -fn ops relation add --id --name --from --to [--via ] [--direction uni] [--status designed] -fn ops relation list [--from ] -fn ops relation show -fn ops relation delete - -# Graph -fn ops graph # ASCII graph de entities y relations - -# Snapshots -fn ops snapshot list # Lista type snapshots -fn ops snapshot check # Compara snapshots vs registry actual -fn ops snapshot update |--all # Re-snapshot desde registry - -# Executions -fn ops execution add --pipeline-id --status success [--started-at ] [--ended-at ] [--records-in N] [--records-out N] [--metrics '{}'] [--error "msg"] -fn ops execution list [--pipeline-id ] [--relation-id ] [-s status] -fn ops execution show - -# Assertions -fn ops assertion add --entity-id --name "close positivo" --kind range --rule "close > 0" --severity critical [--description "..."] -fn ops assertion list [--entity-id ] [--active] [--inactive] -fn ops assertion show # Incluye ultimos 5 resultados -fn ops assertion delete -fn ops assertion eval --entity-id [--execution-id ] [--react] # Evalua assertions activas - -# Assertion results (registro manual) -fn ops assertion result add --assertion-id --status pass|fail|skip [--execution-id ] [--value '{}'] [--message "..."] -fn ops assertion result list [--assertion-id ] [--execution-id ] +# Operations (desde directorio con operations.db) +fn ops init [path] +fn ops entity add|list|show|delete +fn ops relation add|list|show|delete +fn ops graph +fn ops snapshot list|check|update +fn ops execution add|list|show +fn ops assertion add|list|show|delete|eval [--react] +fn ops assertion result add|list ``` `FN_REGISTRY_ROOT` env var permite que `fn ops` acceda a registry.db desde cualquier directorio. --- -## Reglas para añadir funciones nuevas +## Añadir funciones -### Antes de crear +1. Consulta la BD para verificar que no existe algo similar +2. Crea dos archivos: `functions/{domain}/{name}.go` + `functions/{domain}/{name}.md` +3. Ejecuta `./fn index` y verifica con `./fn show {id}` -1. **Consulta la BD** para verificar que no existe algo similar: - ```bash - sqlite3 registry.db "SELECT id, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'tu busqueda');" - ``` -2. **Identifica el dominio** correcto: core, finance, datascience, cybersecurity, o crea uno nuevo si no encaja -3. **Decide la purity**: pure si no tiene side effects, impure si tiene IO/estado/goroutines/tiempo +Frontmatter del .md — ver template completo en `docs/templates/` o con `fn add -k function`. -### Archivos a crear +Reglas de integridad (el indexer las valida): +- Pipeline → siempre impuro + uses_functions no vacio +- Pure → returns_optional: false + error_type: "" +- Impure → error_type obligatorio (usar `error_go_core`) +- tested: true → test_file_path y tests obligatorios +- uses_functions, uses_types, returns, error_type → IDs existentes +- Component → framework obligatorio, returns vacio (usar emits) +- file_path siempre relativa, IDs formato `{name}_{lang}_{domain}` +- Campo `returns` solo para IDs del registry, NO tipos nativos de Go -Cada funcion requiere EXACTAMENTE dos archivos: +## Añadir tipos -1. **Implementacion** `functions/{domain}/{name}.go` — codigo real, compilable -2. **Documentacion** `functions/{domain}/{name}.md` — frontmatter YAML con metadata - -### Formato del .md (frontmatter YAML) - -```yaml ---- -name: nombre_snake_case -kind: function # function | pipeline | component -lang: go # go | python | typescript | sql -domain: core # core | finance | datascience | cybersecurity | ... -version: "1.0.0" -purity: pure # pure | impure -signature: "func NombreCompleto(...) ..." -description: "Descripcion en español de que hace y cuando usarla." -tags: [tag1, tag2, tag3] -uses_functions: [] # IDs de funciones del registry que invoca -uses_types: [] # IDs de tipos del registry que recibe -returns: [] # IDs de tipos del registry que devuelve -returns_optional: false -error_type: "" # ID de tipo de error, obligatorio si impure -imports: [] # dependencias externas fuera del registry -tested: false -tests: [] -test_file_path: "" -file_path: "functions/{domain}/{name}.go" ---- - -## Ejemplo - -` `` go -resultado := MiFuncion(input) -` `` - -## Notas - -Explicacion adicional si es necesario. -``` - -### Reglas de integridad (el indexer las valida) - -| Regla | Condicion | -|---|---| -| Pipeline siempre impuro | `kind: pipeline` → `purity: impure` + `uses_functions` no vacio | -| Pura sin side effects | `purity: pure` → `returns_optional: false` + `error_type: ""` | -| Impura declara errores | `purity: impure` → `error_type` obligatorio (usar `error_go_core`) | -| Tests coherentes | `tested: true` → `test_file_path` y `tests` obligatorios | -| Referencias validas | `uses_functions`, `uses_types`, `returns`, `error_type` deben apuntar a IDs existentes | -| Component tiene framework | `kind: component` → `framework` obligatorio, `returns` vacio (usar `emits`) | -| Rutas relativas | `file_path` siempre relativa a la raiz, nunca absoluta | -| IDs unicos | Formato `{name}_{lang}_{domain}`, colisiones rechazadas | - -### Campo `returns` vs tipo nativo - -El campo `returns` en el .md es para IDs de tipos del registry (ej: `ohlcv_go_finance`), NO para tipos nativos de Go (`float64`, `string`, `bool`). Si la funcion devuelve tipos nativos, deja `returns: []`. - -### Despues de crear - -```bash -# Regenerar el indice -./fn index -# o -CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/ && ./fn index - -# Verificar que se indexo sin errores -./fn show {name}_{lang}_{domain} -``` +Dos archivos: `types/{domain}/{name}.go` + `types/{domain}/{name}.md`. Ver template en `docs/templates/`. --- -## Reglas para añadir tipos nuevos +## Bucle reactivo: CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR -Cada tipo requiere dos archivos: `types/{domain}/{name}.go` y `types/{domain}/{name}.md`. +### 1. CONSTRUIR +- Agente consulta registry → recupera funciones testeadas por FTS sobre `name`, `description`, `tags`. +- Razona sobre composabilidad comparando `returns` con `uses_types`. +- Prioriza funciones puras para el nucleo, aisla impuras en los bordes. +- Registra el pipeline en operations como `status: designed → implemented`. +- **BD:** `registry.db` (functions, types) → `operations.db` (relations, entities) -```yaml ---- -name: nombre_snake_case -lang: go -domain: core -version: "1.0.0" -algebraic: product # product (struct) | sum (interface/union) -definition: | - type MiTipo struct { ... } -description: "Descripcion en español." -tags: [tag1, tag2] -uses_types: [] # IDs de otros tipos que compone (sin auto-referencias) -file_path: "types/{domain}/{name}.go" ---- -``` +### 2. EJECUTAR +- Pipeline corre → inserta registro en `executions` con `duration_ms`, `records_in`, `records_out`, `metrics`. +- `operations.relations.status = running`. +- Si falla → `execution.status = failure`, `error` capturado. +- **BD:** `operations.db` (executions, relations) ---- +### 3. RECOPILAR +- Entities se pueblan — `metadata` contiene los valores concretos de los campos del tipo. +- `types_snapshot` garantiza que `operations.db` es autonomo sin `registry.db`. +- El agente actualiza `entity.status` segun los datos recibidos. +- **BD:** `operations.db` (entities, types_snapshot) -## Convenciones +### 4. ANALIZAR +- Agente evalua todas las `assertions` activas sobre las entities producidas. +- Compara `metrics` de la ejecucion actual con `executions` historicas. +- `critical` falla → `entity.status = corrupted`. +- `warning` falla → `entity.status = stale`. +- Resultados en `assertion_results` con `value` concreto para debugging. +- **BD:** `operations.db` (assertions, assertion_results, entities.status) -- **IDs:** `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`) -- **Nombres:** snake_case para funciones, PascalCase para tipos en Go -- **Paquete Go:** el nombre del directorio (core, finance, datascience, cybersecurity) -- **Tipos en firmas:** usar tipos nativos (float64, []float64, string) para evitar imports circulares entre paquetes de funciones. Documentar los tipos del registry en `uses_types`/`returns` del .md -- **Purity:** puras en el centro, impuras en los bordes. Una pura NUNCA depende de una impura -- **Stubs impuros:** si la implementacion real requiere dependencias externas no disponibles, crear stub con `return ..., fmt.Errorf("not implemented")` y documentar completamente el .md -- **Assertions:** kind es texto libre — puedes inventar nuevos kinds sin tocar schema -- **Proposals:** las crea el bucle reactivo automaticamente (created_by: reactive_loop) o el humano/agente manualmente +### 5. MEJORAR +- Si assertions fallan o metricas degradan → agente escribe en `proposals`. +- `proposals.evidence` referencia los `assertion_ids` y `execution_ids` que lo justifican. +- El humano revisa `proposals.status: pending → approved → implemented`. +- El registry crece de forma controlada y trazable. +- **BD:** `registry.db` (proposals) + +Codigo: `ExecuteAndReact()` en `fn_operations/operations.go` ejecuta pasos 2-4. +CLI: `fn ops assertion eval --entity-id X --react` ejecuta pasos 4-5. +Las assertion rules son expresiones SQL. Campos sin prefijo se reescriben a `json_extract(metadata, '$.campo')`. --- @@ -598,53 +168,8 @@ file_path: "types/{domain}/{name}.go" | Que | Donde | |---|---| -| Codigo | archivos .go / .py / .tsx | -| Documentacion | archivos .md junto al codigo | -| Diseño del schema | carpeta docs/ | -| Indice de busqueda | registry.db (regenerable con `fn index`) | -| Proposals | registry.db tabla proposals (NO regenerable, son datos vivos) | -| Entities, relations | operations.db por proyecto (datos vivos) | -| Executions, assertions | operations.db por proyecto (datos vivos) | - -**Importante:** `fn index` regenera functions y types desde los .md pero NO toca proposals. Las proposals, entities, relations, executions, assertions y assertion_results son datos vivos que persisten. - ---- - -## Codigo Go: patrones clave - -### JSON en columnas TEXT - -Arrays y objetos se guardan como JSON serializado en columnas TEXT: - -```go -marshalStrings([]string) string // ["a","b"] → string -unmarshalStrings(string) []string // string → ["a","b"] -marshalJSON(map[string]any) string // {k:v} → string -unmarshalJSON(string) map[string]any -``` - -### Validacion - -Acumula errores, retorna nil o *ValidationError: - -```go -func ValidateX(x *X) *ValidationError { - var errs []string - if x.Name == "" { errs = append(errs, "name required") } - if len(errs) > 0 { return &ValidationError{ID: x.ID, Errors: errs} } - return nil -} -``` - -### Migraciones - -```go -//go:embed migrations/*.sql -var migrationsFS embed.FS - -func migrate(conn *sql.DB) error { ... } // aplica pendientes en transacciones -``` - -### Cycle detection - -Solo relaciones causales (via != "") se verifican. BFS desde to_entity buscando from_entity. +| Codigo | .go / .py / .tsx | +| Metadata | .md junto al codigo | +| Schema de BDs | `sqlite3 *.db ".schema"` o `docs/` | +| Indice | registry.db (`fn index`) | +| Proposals, entities, executions, assertions | datos vivos en sus BDs | diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md new file mode 100644 index 00000000..804af9e3 --- /dev/null +++ b/.claude/rules/INDEX.md @@ -0,0 +1,15 @@ +# Rules Index + +Reglas operativas del proyecto. Cada archivo es una regla independiente. + +| # | Archivo | Regla | +|---|---------|-------| +| 01 | [db_locations.md](db_locations.md) | Ubicacion de registry.db y operations.db | +| 02 | [ids_naming.md](ids_naming.md) | Formato de IDs y convenciones de nombres | +| 03 | [purity.md](purity.md) | Reglas de pureza funcional | +| 04 | [types_in_signatures.md](types_in_signatures.md) | Tipos nativos en firmas, registry types en .md | +| 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 | +| 09 | [go_packages.md](go_packages.md) | Nombre de paquete Go = nombre del directorio | diff --git a/.claude/rules/assertions.md b/.claude/rules/assertions.md new file mode 100644 index 00000000..e65e236c --- /dev/null +++ b/.claude/rules/assertions.md @@ -0,0 +1,3 @@ +El campo `kind` de assertions es texto libre. Se pueden inventar nuevos kinds sin tocar schema. + +Kinds documentados: `range`, `null`, `statistical`, `consistency`, `freshness`. diff --git a/.claude/rules/db_locations.md b/.claude/rules/db_locations.md new file mode 100644 index 00000000..8227e34a --- /dev/null +++ b/.claude/rules/db_locations.md @@ -0,0 +1,5 @@ +`registry.db` SOLO existe en la raiz del repositorio, NUNCA en subdirectorios ni apps. + +`operations.db` SOLO existe dentro de cada app (`apps/*/operations.db`), NUNCA en la raiz. Cada app tiene su propia operations.db con sus entities, relations y executions independientes. + +Si se detecta un registry.db fuera de la raiz o un operations.db en la raiz, es un error y debe eliminarse. diff --git a/.claude/rules/go_packages.md b/.claude/rules/go_packages.md new file mode 100644 index 00000000..6b6a7152 --- /dev/null +++ b/.claude/rules/go_packages.md @@ -0,0 +1 @@ +El nombre del paquete Go es el nombre del directorio: core, finance, datascience, cybersecurity. diff --git a/.claude/rules/ids_naming.md b/.claude/rules/ids_naming.md new file mode 100644 index 00000000..d417cf23 --- /dev/null +++ b/.claude/rules/ids_naming.md @@ -0,0 +1,3 @@ +IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). + +Nombres de funciones en snake_case. Tipos en PascalCase para Go. diff --git a/.claude/rules/proposals.md b/.claude/rules/proposals.md new file mode 100644 index 00000000..5baa2cc3 --- /dev/null +++ b/.claude/rules/proposals.md @@ -0,0 +1,3 @@ +Las proposals las crea el bucle reactivo automaticamente (`created_by: reactive_loop`) o el humano/agente manualmente. + +Son datos vivos en registry.db — `fn index` NO las toca ni regenera. diff --git a/.claude/rules/purity.md b/.claude/rules/purity.md new file mode 100644 index 00000000..7f1696ed --- /dev/null +++ b/.claude/rules/purity.md @@ -0,0 +1,5 @@ +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 diff --git a/.claude/rules/stubs.md b/.claude/rules/stubs.md new file mode 100644 index 00000000..64522f55 --- /dev/null +++ b/.claude/rules/stubs.md @@ -0,0 +1 @@ +Si la implementacion real requiere dependencias externas no disponibles, crear stub con `return ..., fmt.Errorf("not implemented")` y documentar completamente el .md. diff --git a/.claude/rules/tag_launcher.md b/.claude/rules/tag_launcher.md new file mode 100644 index 00000000..d027e5e6 --- /dev/null +++ b/.claude/rules/tag_launcher.md @@ -0,0 +1,5 @@ +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. diff --git a/.claude/rules/types_in_signatures.md b/.claude/rules/types_in_signatures.md new file mode 100644 index 00000000..595d469b --- /dev/null +++ b/.claude/rules/types_in_signatures.md @@ -0,0 +1,3 @@ +Usar tipos nativos (float64, []float64, string) en firmas Go para evitar imports circulares entre paquetes de funciones. + +Documentar los tipos del registry en `uses_types`/`returns` del .md, no en la firma. From aea21d713c08ad97919a4fadcaa925f8d3b7393e Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 19:14:56 +0100 Subject: [PATCH 3/6] feat: formulario de flags y filtro launcher en pipeline_launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parsea flags de -help de cada pipeline para mostrar formulario de argumentos antes de ejecutar. Filtra pipelines por tag 'launcher'. Corrige selección en historial delegando enter al list antes de leer item. --- apps/pipeline_launcher/go.mod | 3 +- apps/pipeline_launcher/go.sum | 2 + apps/pipeline_launcher/views/history.go | 3 + apps/pipeline_launcher/views/pipelines.go | 171 ++++++++++++++++++++-- apps/pipeline_launcher/views/runner.go | 62 +++++++- 5 files changed, 227 insertions(+), 14 deletions(-) diff --git a/apps/pipeline_launcher/go.mod b/apps/pipeline_launcher/go.mod index 0efb6cda..50d59d93 100644 --- a/apps/pipeline_launcher/go.mod +++ b/apps/pipeline_launcher/go.mod @@ -4,14 +4,15 @@ go 1.22.2 require ( fn-registry v0.0.0 + github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.9.1 github.com/lucasdataproyects/devfactory v0.0.0 ) require ( + github.com/atotto/clipboard v0.1.4 // 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/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/apps/pipeline_launcher/go.sum b/apps/pipeline_launcher/go.sum index 7278f31d..61ff79b5 100644 --- a/apps/pipeline_launcher/go.sum +++ b/apps/pipeline_launcher/go.sum @@ -1,3 +1,5 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 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= diff --git a/apps/pipeline_launcher/views/history.go b/apps/pipeline_launcher/views/history.go index df19bccd..6e1d00e8 100644 --- a/apps/pipeline_launcher/views/history.go +++ b/apps/pipeline_launcher/views/history.go @@ -106,6 +106,9 @@ func (m HistoryModel) Update(msg tea.Msg) (HistoryModel, tea.Cmd) { m.spinner = tui.NewSpinner("Loading history...") return m, tea.Batch(m.spinner.Init(), m.loadHistory()) case "enter": + // Delegate enter to list first so it selects the cursor item + updated, _ := m.list.Update(msg) + m.list = updated.(tui.FilteredListModel) if item := m.list.SelectedItem(); item != nil { e := item.Value.(ops.Execution) m.detail = formatExecution(e) diff --git a/apps/pipeline_launcher/views/pipelines.go b/apps/pipeline_launcher/views/pipelines.go index a75b6f74..190e2c6a 100644 --- a/apps/pipeline_launcher/views/pipelines.go +++ b/apps/pipeline_launcher/views/pipelines.go @@ -7,6 +7,7 @@ import ( ops "fn-registry/fn_operations" "fn-registry/registry" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lucasdataproyects/devfactory/tui" @@ -17,12 +18,14 @@ type pipelinesState int const ( pipelinesLoading pipelinesState = iota pipelinesList + pipelinesArgs pipelinesRunning pipelinesOutput ) type pipelinesLoadedMsg []registry.Function type pipelineFinishedMsg RunResult +type pipelineFlagsMsg []PipelineFlag // PipelinesModel lists and launches pipelines. type PipelinesModel struct { @@ -31,6 +34,10 @@ type PipelinesModel struct { spinner tui.SpinnerModel styles tui.Styles pipelines []registry.Function + selectedFn *registry.Function + flags []PipelineFlag + inputs []textinput.Model + focusIdx int output string lastResult *RunResult scrollOff int @@ -66,10 +73,67 @@ func (m PipelinesModel) loadPipelines() tea.Cmd { if err != nil { return pipelinesLoadedMsg(nil) } - return pipelinesLoadedMsg(fns) + // Only show pipelines tagged with "launcher" + var launchable []registry.Function + for _, f := range fns { + for _, t := range f.Tags { + if t == "launcher" { + launchable = append(launchable, f) + break + } + } + } + return pipelinesLoadedMsg(launchable) } } +// buildInputs creates a textinput for each flag, pre-filled with defaults. +func (m *PipelinesModel) buildInputs() tea.Cmd { + m.inputs = make([]textinput.Model, len(m.flags)) + for i, f := range m.flags { + ti := textinput.New() + ti.CharLimit = 256 + ti.Width = 40 + if f.Default != "" { + ti.SetValue(f.Default) + } + if f.Required { + ti.Placeholder = "(requerido)" + } + m.inputs[i] = ti + } + m.focusIdx = 0 + if len(m.inputs) > 0 { + m.inputs[0].Focus() + return textinput.Blink + } + return nil +} + +func (m *PipelinesModel) focusInput(idx int) tea.Cmd { + if idx < 0 || idx >= len(m.inputs) { + return nil + } + for i := range m.inputs { + m.inputs[i].Blur() + } + m.focusIdx = idx + m.inputs[idx].Focus() + return textinput.Blink +} + +// collectArgs builds CLI args from the form inputs. +func (m PipelinesModel) collectArgs() []string { + var args []string + for i, f := range m.flags { + val := strings.TrimSpace(m.inputs[i].Value()) + if val != "" { + args = append(args, "--"+f.Name, val) + } + } + return args +} + func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) { switch msg := msg.(type) { case pipelinesLoadedMsg: @@ -86,19 +150,23 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) { m.state = pipelinesList return m, nil + case pipelineFlagsMsg: + m.flags = []PipelineFlag(msg) + cmd := m.buildInputs() + return m, cmd + case pipelineFinishedMsg: result := RunResult(msg) m.lastResult = &result - // Build output var sb strings.Builder if result.Status == ops.ExecSuccess { sb.WriteString("[OK] ") } else { sb.WriteString("[FAIL] ") } - sb.WriteString(fmt.Sprintf("Pipeline: %s\n", result.PipelineID)) - sb.WriteString(fmt.Sprintf("Execution: %s\n", result.ExecID)) - sb.WriteString(fmt.Sprintf("Duration: %dms\n", result.DurationMs)) + fmt.Fprintf(&sb, "Pipeline: %s\n", result.PipelineID) + fmt.Fprintf(&sb, "Execution: %s\n", result.ExecID) + fmt.Fprintf(&sb, "Duration: %dms\n", result.DurationMs) sb.WriteString("\n--- stdout ---\n") if result.Stdout != "" { sb.WriteString(result.Stdout) @@ -110,7 +178,7 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) { sb.WriteString(result.Stderr) } if result.Err != nil { - sb.WriteString(fmt.Sprintf("\n--- error ---\n%v", result.Err)) + fmt.Fprintf(&sb, "\n--- error ---\n%v", result.Err) } m.output = sb.String() m.state = pipelinesOutput @@ -126,12 +194,42 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) { m.spinner = tui.NewSpinner("Loading pipelines...") return m, tea.Batch(m.spinner.Init(), m.loadPipelines()) case "enter": + updated, _ := m.list.Update(msg) + m.list = updated.(tui.FilteredListModel) if item := m.list.SelectedItem(); item != nil { fn := item.Value.(registry.Function) - m.state = pipelinesRunning - m.spinner = tui.NewSpinner(fmt.Sprintf("Running %s...", fn.Name)) - return m, tea.Batch(m.spinner.Init(), m.runPipelineCmd(&fn)) + m.selectedFn = &fn + m.flags = nil + m.inputs = nil + m.state = pipelinesArgs + root := m.registryRoot + fnCopy := fn + return m, func() tea.Msg { + return pipelineFlagsMsg(GetPipelineFlags(&fnCopy, root)) + } } + return m, nil + } + case pipelinesArgs: + switch msg.String() { + case "tab", "down": + cmd := m.focusInput((m.focusIdx + 1) % max(len(m.inputs), 1)) + return m, cmd + case "shift+tab", "up": + idx := m.focusIdx - 1 + if idx < 0 { + idx = max(len(m.inputs)-1, 0) + } + cmd := m.focusInput(idx) + return m, cmd + case "ctrl+enter", "ctrl+s": + args := m.collectArgs() + m.state = pipelinesRunning + m.spinner = tui.NewSpinner(fmt.Sprintf("Running %s...", m.selectedFn.Name)) + return m, tea.Batch(m.spinner.Init(), m.runPipelineCmd(m.selectedFn, args)) + case "esc": + m.state = pipelinesList + return m, nil } case pipelinesOutput: switch msg.String() { @@ -157,16 +255,20 @@ func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) { var listModel tea.Model listModel, cmd = m.list.Update(msg) m.list = listModel.(tui.FilteredListModel) + case pipelinesArgs: + if m.focusIdx >= 0 && m.focusIdx < len(m.inputs) { + m.inputs[m.focusIdx], cmd = m.inputs[m.focusIdx].Update(msg) + } } return m, cmd } -func (m PipelinesModel) runPipelineCmd(fn *registry.Function) tea.Cmd { +func (m PipelinesModel) runPipelineCmd(fn *registry.Function, args []string) tea.Cmd { regRoot := m.registryRoot opsDB := m.opsDB fnCopy := *fn return func() tea.Msg { - result := RunPipeline(&fnCopy, regRoot, opsDB) + result := RunPipeline(&fnCopy, regRoot, opsDB, args) return pipelineFinishedMsg(result) } } @@ -174,6 +276,9 @@ func (m PipelinesModel) runPipelineCmd(fn *registry.Function) tea.Cmd { // HandleBack retrocede un nivel. Retorna true si ya en estado base. func (m *PipelinesModel) HandleBack() bool { switch m.state { + case pipelinesArgs: + m.state = pipelinesList + return false case pipelinesOutput: m.state = pipelinesList return false @@ -192,6 +297,8 @@ func (m PipelinesModel) View() string { } help := m.styles.Muted.Render(" Enter: launch │ r: refresh │ /: filter") return m.list.View() + "\n" + help + case pipelinesArgs: + return m.renderArgsForm() case pipelinesRunning: return m.spinner.View() case pipelinesOutput: @@ -200,6 +307,48 @@ func (m PipelinesModel) View() string { return "" } +func (m PipelinesModel) renderArgsForm() string { + header := m.styles.Header.Render(m.selectedFn.Name) + + var parts []string + parts = append(parts, header, "") + + if len(m.flags) == 0 { + parts = append(parts, m.styles.Muted.Render(" Loading flags...")) + } else if len(m.inputs) == 0 { + parts = append(parts, m.styles.Muted.Render(" No flags available. Ctrl+S to run.")) + } else { + for i, f := range m.flags { + marker := " " + if f.Required { + marker = m.styles.Error.Render("* ") + } + + name := fmt.Sprintf("--%-16s", f.Name) + cursor := " " + if i == m.focusIdx { + cursor = m.styles.Info.Render("> ") + } + + label := fmt.Sprintf("%s%s%s", cursor, marker, m.styles.Label.Render(name)) + input := m.inputs[i].View() + + desc := f.Desc + if f.Default != "" { + desc += m.styles.Muted.Render(fmt.Sprintf(" (default: %s)", f.Default)) + } + + parts = append(parts, label+input) + parts = append(parts, " "+m.styles.Muted.Render(desc)) + } + } + + parts = append(parts, "") + parts = append(parts, m.styles.Muted.Render(" ↑/↓: navigate │ Ctrl+S: run │ Esc: cancel")) + + return lipgloss.JoinVertical(lipgloss.Left, parts...) +} + func (m PipelinesModel) renderOutput() string { lines := splitLines(m.output) maxLines := 20 diff --git a/apps/pipeline_launcher/views/runner.go b/apps/pipeline_launcher/views/runner.go index 3dfdae22..9f41f592 100644 --- a/apps/pipeline_launcher/views/runner.go +++ b/apps/pipeline_launcher/views/runner.go @@ -5,12 +5,69 @@ import ( "fmt" "os/exec" "path/filepath" + "regexp" + "strings" "time" ops "fn-registry/fn_operations" "fn-registry/registry" ) +// PipelineFlag describes a CLI flag parsed from -help output. +type PipelineFlag struct { + Name string // e.g. "project" + Type string // e.g. "string" + Desc string // description text + Default string // default value, empty if none + Required bool // true if no default +} + +var flagLineRe = regexp.MustCompile(`^\s+-(\S+)\s+(\S+)$`) +var defaultRe = regexp.MustCompile(`\(default "(.*)"\)`) + +// GetPipelineFlags runs `go run . -help` and parses the flag output. +func GetPipelineFlags(fn *registry.Function, registryRoot string) []PipelineFlag { + absPath := filepath.Join(registryRoot, fn.FilePath) + dir := filepath.Dir(absPath) + + cmd := exec.Command("go", "run", ".", "-help") + cmd.Dir = dir + var stderr bytes.Buffer + cmd.Stderr = &stderr + cmd.Run() // -help exits with code 2, ignore error + + return parseFlags(stderr.String()) +} + +func parseFlags(output string) []PipelineFlag { + var flags []PipelineFlag + lines := strings.Split(output, "\n") + + for i := 0; i < len(lines); i++ { + m := flagLineRe.FindStringSubmatch(lines[i]) + if m == nil { + continue + } + f := PipelineFlag{Name: m[1], Type: m[2]} + + // Next line is the description + if i+1 < len(lines) { + desc := strings.TrimSpace(lines[i+1]) + if dm := defaultRe.FindStringSubmatch(desc); dm != nil { + f.Default = dm[1] + f.Desc = strings.TrimSpace(defaultRe.ReplaceAllString(desc, "")) + } else { + f.Desc = desc + } + i++ + } + + f.Required = f.Default == "" && !strings.Contains(strings.ToLower(f.Desc), "opcional") + flags = append(flags, f) + } + return flags +} + // RunResult holds the outcome of a pipeline execution. type RunResult struct { Stdout string @@ -23,13 +80,14 @@ type RunResult struct { } // RunPipeline executes a pipeline as a subprocess and records the execution. -func RunPipeline(fn *registry.Function, registryRoot string, opsDB *ops.DB) RunResult { +func RunPipeline(fn *registry.Function, registryRoot string, opsDB *ops.DB, args []string) RunResult { absPath := filepath.Join(registryRoot, fn.FilePath) dir := filepath.Dir(absPath) startedAt := time.Now().UTC() - cmd := exec.Command("go", "run", ".") + cmdArgs := append([]string{"run", "."}, args...) + cmd := exec.Command("go", cmdArgs...) cmd.Dir = dir var stdout, stderr bytes.Buffer From ba6436ae2f8ac8bbb00e6324a95b019165439cba Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 19:15:01 +0100 Subject: [PATCH 4/6] =?UTF-8?q?chore:=20ajustar=20tags=20de=20pipelines=20?= =?UTF-8?q?seg=C3=BAn=20regla=20tag=5Flauncher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade tag 'launcher' a init_metabase para que aparezca en la TUI. Elimina tag 'launcher' de pipeline_launcher ya que es una TUI interactiva, no un subproceso lanzable. --- functions/pipelines/init_metabase.md | 2 +- functions/pipelines/pipeline_launcher.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/pipelines/init_metabase.md b/functions/pipelines/init_metabase.md index 76f620d0..a7cbc232 100644 --- a/functions/pipelines/init_metabase.md +++ b/functions/pipelines/init_metabase.md @@ -7,7 +7,7 @@ version: "1.0.0" purity: impure signature: "func main() — Despliega stack Metabase + Postgres en Docker" description: "Pipeline que inicializa un contenedor Metabase con su base de datos Postgres. Crea red Docker, pull de imágenes, inicia Postgres con volume persistente, espera health check y lanza Metabase conectado." -tags: [docker, metabase, postgres, pipeline, infra, analytics] +tags: [docker, metabase, postgres, pipeline, infra, analytics, launcher] uses_functions: - docker_create_network_go_infra - docker_pull_image_go_infra diff --git a/functions/pipelines/pipeline_launcher.md b/functions/pipelines/pipeline_launcher.md index 96365c69..881dfac2 100644 --- a/functions/pipelines/pipeline_launcher.md +++ b/functions/pipelines/pipeline_launcher.md @@ -7,7 +7,7 @@ version: "1.0.0" purity: impure signature: "func main() — TUI fullscreen para lanzar pipelines y registrar ejecuciones" description: "TUI interactiva que lista pipelines del registry, permite lanzarlos como subprocesos y registra cada ejecución en operations.db. Dos tabs: Pipelines (filtro + launch) y History (historial)." -tags: [tui, pipeline, launcher, infra, operations, bubbletea, devfactory] +tags: [tui, pipeline, infra, operations, bubbletea, devfactory] uses_functions: - docker_tui_go_infra uses_types: From 989ce5ba0ff903b0d84b45d6b7bb427d1195ca93 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 19:15:08 +0100 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20gitignore=20para=20operations.db?= =?UTF-8?q?=20en=20ra=C3=ADz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade reglas para ignorar operations.db, journal y wal en la raíz del repo, reforzando la regla de que solo deben existir dentro de apps/. --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 35b6654d..edc2f63a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ -# SQLite index (regenerable con fn index) +# SQLite index (regenerable con fn index) — SOLO en raiz registry.db registry.db-journal registry.db-wal +# operations.db NO debe existir en raiz (solo en apps/*) +/operations.db +/operations.db-journal +/operations.db-wal + # Binario CLI /fn From f7915e72e288b3b2e37ba1d63e8aa03dd12f26c5 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 19:16:40 +0100 Subject: [PATCH 6/6] chore: gitignore operations.db en todas las ubicaciones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambia patrón de /operations.db (solo raíz) a **/operations.db para ignorar los archivos de datos vivos en cualquier app. La template en fn_operations/project_template/ sigue trackeada por estar ya en el índice. --- .gitignore | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index edc2f63a..84ae434f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,11 @@ registry.db registry.db-journal registry.db-wal -# operations.db NO debe existir en raiz (solo en apps/*) -/operations.db -/operations.db-journal -/operations.db-wal +# operations.db — datos vivos, cada app genera el suyo con fn ops init +**/operations.db +**/operations.db-journal +**/operations.db-wal +**/operations.db-shm # Binario CLI /fn