chore: auto-commit (286 archivos)

- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 16:33:22 +02:00
parent 0b9af8f1bb
commit a03675113a
281 changed files with 12596 additions and 19526 deletions
+80
View File
@@ -0,0 +1,80 @@
package infra
import (
"os"
"path/filepath"
)
// AppLocationViolation describes an artefact found in a language-specific
// directory where it does not belong (e.g. cpp/apps/, python/analysis/).
type AppLocationViolation struct {
Path string // relative from repoRoot (e.g. "cpp/apps/foo")
Kind string // "app" | "analysis"
Lang string // "cpp" | "python" | "bash" | "frontend"
}
// AuditAppLocation scans language-specific directories that must NOT contain
// artefacts (apps or analyses) and returns every subdirectory that holds an
// app.md or analysis.md manifest.
//
// Prohibited directories checked:
// - cpp/apps/, cpp/analysis/
// - python/apps/, python/analysis/
// - bash/apps/, bash/analysis/
// - frontend/apps/, frontend/analysis/
//
// If a prohibited directory does not exist it is silently skipped.
// Artefacts under the canonical apps/ and analysis/ roots are not flagged.
func AuditAppLocation(repoRoot string) ([]AppLocationViolation, error) {
type candidate struct {
lang string
kind string
dir string // relative dir inside repoRoot
}
candidates := []candidate{
{lang: "cpp", kind: "app", dir: "cpp/apps"},
{lang: "cpp", kind: "analysis", dir: "cpp/analysis"},
{lang: "python", kind: "app", dir: "python/apps"},
{lang: "python", kind: "analysis", dir: "python/analysis"},
{lang: "bash", kind: "app", dir: "bash/apps"},
{lang: "bash", kind: "analysis", dir: "bash/analysis"},
{lang: "frontend", kind: "app", dir: "frontend/apps"},
{lang: "frontend", kind: "analysis", dir: "frontend/analysis"},
}
manifest := map[string]string{
"app": "app.md",
"analysis": "analysis.md",
}
var out []AppLocationViolation
for _, c := range candidates {
absDir := filepath.Join(repoRoot, c.dir)
entries, err := os.ReadDir(absDir)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
mf := manifest[c.kind]
for _, e := range entries {
if !e.IsDir() {
continue
}
mdPath := filepath.Join(absDir, e.Name(), mf)
if _, err := os.Stat(mdPath); err == nil {
out = append(out, AppLocationViolation{
Path: filepath.Join(c.dir, e.Name()),
Kind: c.kind,
Lang: c.lang,
})
}
}
}
return out, nil
}
+55
View File
@@ -0,0 +1,55 @@
---
name: audit_app_location
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func AuditAppLocation(repoRoot string) ([]AppLocationViolation, error)"
description: "Detecta artefactos (apps y analyses) ubicados en carpetas de lenguaje (cpp/apps/, python/apps/, bash/apps/, frontend/apps/, y sus equivalentes /analysis/). Estas ubicaciones violan la convencion del registry (issue 0096): los artefactos deben vivir en apps/ o analysis/ en la raiz, o en projects/*/apps/ y projects/*/analysis/. Retorna una lista de AppLocationViolation con la ruta relativa, kind (app|analysis) y lang (cpp|python|bash|frontend)."
tags: [doctor, audit, location, artefacts, registry]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["os", "path/filepath"]
params:
- name: repoRoot
desc: "Ruta absoluta a la raiz del repositorio fn_registry. Todas las rutas en las violaciones son relativas a este directorio."
output: "Lista de AppLocationViolation con (Path, Kind, Lang) por cada artefacto mal ubicado. Vacia si no hay violaciones. Error si un directorio prohibido existe pero no es legible."
tested: true
tests:
- "detecta app.md en directorio prohibido cpp/apps"
- "directorios prohibidos inexistentes no producen error"
- "detecta analysis.md en directorio prohibido python/analysis"
test_file_path: "functions/infra/audit_app_location_test.go"
file_path: "functions/infra/audit_app_location.go"
---
## Ejemplo
```go
violations, err := AuditAppLocation("/home/lucas/fn_registry")
if err != nil {
log.Fatal(err)
}
for _, v := range violations {
fmt.Printf("[%s/%s] %s → mover a apps/%s o projects/.../apps/%s\n",
v.Lang, v.Kind, v.Path,
filepath.Base(v.Path), filepath.Base(v.Path))
}
// Ejemplo de salida:
// [cpp/app] cpp/apps/my_tool → mover a apps/my_tool o projects/.../apps/my_tool
```
## Cuando usarla
Usar como check de `fn doctor artefacts` o en pre-commit para detectar artefactos que se crearon en la carpeta del lenguaje equivocada. Invocar despues de `fn index` para validar el estado del repo.
## Gotchas
- Solo revisa subdirectorios de nivel 1 dentro de cada directorio prohibido. No escanea recursivamente.
- Un subdirectorio sin `app.md` ni `analysis.md` NO se reporta (ej. `cpp/apps/bar/` sin manifest = ignorado).
- Los artefactos en `apps/`, `analysis/`, `projects/*/apps/` y `projects/*/analysis/` son ubicaciones canonicas y NO se comprueban.
- `AppLocationViolation` esta definida en el mismo paquete (`infra`), no como tipo separado del registry.
@@ -0,0 +1,92 @@
package infra
import (
"os"
"path/filepath"
"testing"
)
func TestAuditAppLocation(t *testing.T) {
t.Run("detecta app.md en directorio prohibido cpp/apps", func(t *testing.T) {
root := t.TempDir()
// Violacion: cpp/apps/foo tiene app.md
fooDir := filepath.Join(root, "cpp", "apps", "foo")
if err := os.MkdirAll(fooDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(fooDir, "app.md"), []byte("name: foo\n"), 0o644); err != nil {
t.Fatal(err)
}
// Ignorado: cpp/apps/bar sin app.md
barDir := filepath.Join(root, "cpp", "apps", "bar")
if err := os.MkdirAll(barDir, 0o755); err != nil {
t.Fatal(err)
}
// Ignorado: apps/baz tiene app.md pero es el lugar canonico
bazDir := filepath.Join(root, "apps", "baz")
if err := os.MkdirAll(bazDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(bazDir, "app.md"), []byte("name: baz\n"), 0o644); err != nil {
t.Fatal(err)
}
violations, err := AuditAppLocation(root)
if err != nil {
t.Fatalf("AuditAppLocation returned error: %v", err)
}
if len(violations) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(violations), violations)
}
v := violations[0]
wantPath := filepath.Join("cpp", "apps", "foo")
if v.Path != wantPath {
t.Errorf("Path: got %q, want %q", v.Path, wantPath)
}
if v.Kind != "app" {
t.Errorf("Kind: got %q, want %q", v.Kind, "app")
}
if v.Lang != "cpp" {
t.Errorf("Lang: got %q, want %q", v.Lang, "cpp")
}
})
t.Run("directorios prohibidos inexistentes no producen error", func(t *testing.T) {
root := t.TempDir()
violations, err := AuditAppLocation(root)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(violations) != 0 {
t.Errorf("expected 0 violations, got %d", len(violations))
}
})
t.Run("detecta analysis.md en directorio prohibido python/analysis", func(t *testing.T) {
root := t.TempDir()
anaDir := filepath.Join(root, "python", "analysis", "my_analysis")
if err := os.MkdirAll(anaDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(anaDir, "analysis.md"), []byte("name: my_analysis\n"), 0o644); err != nil {
t.Fatal(err)
}
violations, err := AuditAppLocation(root)
if err != nil {
t.Fatalf("AuditAppLocation returned error: %v", err)
}
if len(violations) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(violations), violations)
}
v := violations[0]
if v.Kind != "analysis" || v.Lang != "python" {
t.Errorf("got Kind=%q Lang=%q, want Kind=analysis Lang=python", v.Kind, v.Lang)
}
})
}
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: pure
signature: "func ConfigValidate(cfg any) ConfigValidation"
description: "Valida una struct de configuracion usando struct tags. Acumula todos los errores (required, format:email, format:url, min, max, oneof) sin detener en el primero. Retorna ConfigValidation con IsValid=true si no hay errores."
tags: [config, validation, env, infra, reflect, pendiente-usar]
tags: [config, validation, env, infra, reflect, pendiente-usar, validator]
uses_functions: []
uses_types: [config_validation_go_infra, config_error_go_infra]
returns: [config_validation_go_infra]
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure
signature: "func DBInsertRow(db *sql.DB, table string, row map[string]any) (int64, error)"
description: "Genera y ejecuta un INSERT de una sola fila desde un map columna→valor. Retorna el last insert ID. Sanitiza nombres de tabla y columnas."
tags: [database, sql, insert, row, dynamic]
tags: [database, sql, insert, row, dynamic, sink]
uses_functions: []
uses_types: []
returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: pure
signature: "func FileValidateType(header []byte, allowedTypes []string) (string, bool)"
description: "Detecta el MIME type real de un archivo a partir de sus primeros bytes (magic bytes) y verifica que esta en la lista de tipos permitidos. Mas seguro que confiar en el header Content-Type del request."
tags: [file, validate, mime, magic, security, upload, infra]
tags: [file, validate, mime, magic, security, upload, infra, validator]
uses_functions: []
uses_types: []
returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure
signature: "func HttpDownloadFile(url, destPath string, headers map[string]string, timeout time.Duration) (int64, error)"
description: "Descarga url en destPath en streaming con io.Copy. Crea directorios con os.MkdirAll. Usa archivo temporal + rename para atomicidad (no deja archivo corrupto si falla). Retorna bytes escritos."
tags: [http, download, file, streaming, atomic, network, stdlib, infra, pendiente-usar]
tags: [http, download, file, streaming, atomic, network, stdlib, infra, pendiente-usar, extractor]
uses_functions: []
uses_types: []
returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure
signature: "func HttpGetJSON(url string, headers map[string]string, timeout time.Duration) (map[string]any, error)"
description: "GET request que espera JSON. Agrega Accept: application/json automaticamente. Retorna error con status code si >= 400. Siempre cierra body con defer."
tags: [http, json, get, client, network, stdlib, infra, pendiente-usar]
tags: [http, json, get, client, network, stdlib, infra, pendiente-usar, extractor]
uses_functions: []
uses_types: []
returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure
signature: "func HttpPostJSON(url string, body any, headers map[string]string, timeout time.Duration) (map[string]any, error)"
description: "POST request con body JSON serializado con json.Marshal. Agrega Content-Type: application/json y Accept: application/json. Retorna error con status code si >= 400."
tags: [http, json, post, client, network, stdlib, infra, pendiente-usar]
tags: [http, json, post, client, network, stdlib, infra, pendiente-usar, sink]
uses_functions: []
uses_types: []
returns: []