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:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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: []
|
||||
|
||||
Reference in New Issue
Block a user