merge: quick/ops-db-cleanup — operations.db en apps, reglas y mejoras launcher
This commit is contained in:
+97
-572
@@ -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 <id>
|
||||
fn add -k function # Template
|
||||
|
||||
### Proposals
|
||||
|
||||
```bash
|
||||
fn proposal add --kind new_function --title "..." --created-by agent [--target-id <id>] [--evidence '{}'] [--description "..."]
|
||||
# Proposals
|
||||
fn proposal add --kind new_function --title "..." --created-by agent [--target-id <id>]
|
||||
fn proposal list [-k kind] [-s status]
|
||||
fn proposal show <id>
|
||||
fn proposal update <id> --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 <id> --name <n> --type-ref <tipo> --source <src> [--metadata '{}'] [--domain d] [--tags t1,t2]
|
||||
fn ops entity list [--domain d] [--status s]
|
||||
fn ops entity show <id>
|
||||
fn ops entity delete <id>
|
||||
|
||||
# Relations
|
||||
fn ops relation add --id <id> --name <n> --from <eid> --to <eid> [--via <fn_id>] [--direction uni] [--status designed]
|
||||
fn ops relation list [--from <eid>]
|
||||
fn ops relation show <id>
|
||||
fn ops relation delete <id>
|
||||
|
||||
# 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 <type_id>|--all # Re-snapshot desde registry
|
||||
|
||||
# Executions
|
||||
fn ops execution add --pipeline-id <id> --status success [--started-at <ts>] [--ended-at <ts>] [--records-in N] [--records-out N] [--metrics '{}'] [--error "msg"]
|
||||
fn ops execution list [--pipeline-id <id>] [--relation-id <id>] [-s status]
|
||||
fn ops execution show <id>
|
||||
|
||||
# Assertions
|
||||
fn ops assertion add --entity-id <id> --name "close positivo" --kind range --rule "close > 0" --severity critical [--description "..."]
|
||||
fn ops assertion list [--entity-id <id>] [--active] [--inactive]
|
||||
fn ops assertion show <id> # Incluye ultimos 5 resultados
|
||||
fn ops assertion delete <id>
|
||||
fn ops assertion eval --entity-id <id> [--execution-id <id>] [--react] # Evalua assertions activas
|
||||
|
||||
# Assertion results (registro manual)
|
||||
fn ops assertion result add --assertion-id <id> --status pass|fail|skip [--execution-id <id>] [--value '{}'] [--message "..."]
|
||||
fn ops assertion result list [--assertion-id <id>] [--execution-id <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 |
|
||||
|
||||
@@ -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 |
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
El nombre del paquete Go es el nombre del directorio: core, finance, datascience, cybersecurity.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+7
-1
@@ -1,8 +1,14 @@
|
||||
# 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 — 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
|
||||
|
||||
|
||||
Binary file not shown.
@@ -21,7 +21,7 @@ func Default() Config {
|
||||
|
||||
return Config{
|
||||
RegistryDB: filepath.Join(root, "registry.db"),
|
||||
OperationsDB: filepath.Join(root, "operations.db"),
|
||||
OperationsDB: filepath.Join(root, "apps", "pipeline_launcher", "operations.db"),
|
||||
RegistryRoot: root,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user