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
+79
View File
@@ -0,0 +1,79 @@
package core
import (
"fmt"
"strconv"
"strings"
)
// CronExplain returns a short human-readable description of a cron expression.
// Supports 5-field standard cron and @hourly/@daily/@weekly/@monthly shortcuts.
// Returns the raw expression if the pattern is not recognized.
func CronExplain(expr string) string {
expr = strings.TrimSpace(expr)
// Handle shortcuts
switch expr {
case "@hourly":
return "hourly"
case "@daily", "@midnight":
return "daily"
case "@weekly":
return "weekly"
case "@monthly":
return "monthly"
case "@yearly", "@annually":
return "yearly"
}
fields := strings.Fields(expr)
if len(fields) < 5 {
return expr
}
// Use first 5 fields (ignore optional 6th seconds/year field)
minute := fields[0]
hour := fields[1]
dom := fields[2] // day of month
month := fields[3]
dow := fields[4] // day of week
// "every N minutes": */N in minute field, rest are *
if strings.HasPrefix(minute, "*/") && dom == "*" && month == "*" && dow == "*" {
n := minute[2:]
if n == "1" {
return "every minute"
}
return fmt.Sprintf("every %s minutes", n)
}
// "every N hours": plain minute "0", */N in hour, rest are *
if strings.HasPrefix(hour, "*/") && minute == "0" && dom == "*" && month == "*" && dow == "*" {
n := hour[2:]
if n == "1" {
return "every hour"
}
return fmt.Sprintf("every %s hours", n)
}
// "daily at HH:MM": plain numbers for minute and hour, rest are *
if dom == "*" && month == "*" && dow == "*" {
m, errM := strconv.Atoi(minute)
h, errH := strconv.Atoi(hour)
if errM == nil && errH == nil {
return fmt.Sprintf("daily at %02d:%02d", h, m)
}
}
// "weekdays at HH:MM": plain minute/hour, dow == "1-5", dom/month are *
if dom == "*" && month == "*" && dow == "1-5" {
m, errM := strconv.Atoi(minute)
h, errH := strconv.Atoi(hour)
if errM == nil && errH == nil {
return fmt.Sprintf("weekdays at %02d:%02d", h, m)
}
}
// Not recognized — return raw expression
return expr
}
+61
View File
@@ -0,0 +1,61 @@
---
name: cron_explain
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func CronExplain(expr string) string"
description: "Convierte una expresion cron (5 campos o shortcut @daily/@hourly/etc.) en una frase humana corta. Reconoce patrones comunes: every N minutes/hours, daily/weekdays at HH:MM, y shortcuts. Devuelve el expr crudo si no encaja en ningun patron. Sin dependencias externas, solo stdlib."
tags: ["cron", "scheduler", "humanize"]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["fmt", "strconv", "strings"]
tested: true
tests:
- "every 15 minutes"
- "daily at 00:00"
- "weekdays at 09:00"
- "every 30 minutes"
- "every 2 hours"
- "shortcut @hourly"
- "shortcut @daily"
- "shortcut @weekly"
- "shortcut @monthly"
- "unknown returns raw"
- "invalid too few fields"
- "every minute"
- "daily at 14:30"
- "every hour zero min"
- "shortcut @midnight"
- "weekdays at 08:00"
test_file_path: "functions/core/cron_explain_test.go"
file_path: "functions/core/cron_explain.go"
params:
- name: expr
desc: "Expresion cron de 5 campos (min hora dom mes dow) o shortcut (@hourly, @daily, @weekly, @monthly, @yearly). Se ignoran campos extra (6to campo segundos/year)."
output: "Frase legible en ingles: 'every N minutes', 'daily at HH:MM', 'weekdays at HH:MM', 'hourly', 'daily', 'weekly', 'monthly'. Devuelve expr sin modificar si el patron no es reconocido."
---
## Ejemplo
```go
fmt.Println(CronExplain("*/15 * * * *")) // every 15 minutes
fmt.Println(CronExplain("0 0 * * *")) // daily at 00:00
fmt.Println(CronExplain("0 9 * * 1-5")) // weekdays at 09:00
fmt.Println(CronExplain("*/30 * * * *")) // every 30 minutes
fmt.Println(CronExplain("0 */2 * * *")) // every 2 hours
fmt.Println(CronExplain("@hourly")) // hourly
fmt.Println(CronExplain("5 4 * * 0")) // 5 4 * * 0 (not recognized, returned raw)
```
## Cuando usarla
Cuando necesites mostrar al usuario una descripcion legible de un schedule cron en una UI, log o CLI. Antes de renderizar un campo `cron_expr` en un dashboard o TUI.
## Gotchas
Funcion pura — nunca falla ni entra en panico. Patrones reconocidos son los comunes; expresiones con listas (`1,3,5`), rangos en horas, o combinaciones complejas devuelven el expr crudo sin error.
+37
View File
@@ -0,0 +1,37 @@
package core
import "testing"
func TestCronExplain(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"every 15 minutes", "*/15 * * * *", "every 15 minutes"},
{"daily at 00:00", "0 0 * * *", "daily at 00:00"},
{"weekdays at 09:00", "0 9 * * 1-5", "weekdays at 09:00"},
{"every 30 minutes", "*/30 * * * *", "every 30 minutes"},
{"every 2 hours", "0 */2 * * *", "every 2 hours"},
{"shortcut @hourly", "@hourly", "hourly"},
{"shortcut @daily", "@daily", "daily"},
{"shortcut @weekly", "@weekly", "weekly"},
{"shortcut @monthly", "@monthly", "monthly"},
{"unknown returns raw", "5 4 * * 0", "5 4 * * 0"},
{"invalid too few fields", "bad", "bad"},
{"every minute", "*/1 * * * *", "every minute"},
{"daily at 14:30", "30 14 * * *", "daily at 14:30"},
{"every hour zero min", "0 */1 * * *", "every hour"},
{"shortcut @midnight", "@midnight", "daily"},
{"weekdays at 08:00", "0 8 * * 1-5", "weekdays at 08:00"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := CronExplain(tc.input)
if got != tc.want {
t.Errorf("CronExplain(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: pure
signature: "func CronMatch(sched CronSchedule, t time.Time) bool"
description: "Verifica si un instante de tiempo coincide con un cron schedule. Compara los 5 campos (minuto, hora, dia del mes, mes, dia de la semana) y retorna true si todos coinciden."
tags: [cron, scheduling, matching, time, pure, pendiente-usar]
tags: [cron, scheduling, matching, time, pure, pendiente-usar, scheduler]
uses_functions: []
uses_types: [cron_schedule_go_core]
returns: []
+1
View File
@@ -20,6 +20,7 @@ type DagStep struct {
Command string
Script string
Args []string
Function string `json:"function,omitempty"` // ID de funcion del registry (ej "audit_capability_groups_go_infra"). Si set, Command/Script se ignoran; el executor construye "fn run <Function> <Args...>".
Shell string
Dir string
Depends []string
+2
View File
@@ -15,6 +15,7 @@ type rawDagStep struct {
Command string `yaml:"command"`
Script string `yaml:"script"`
Args []string `yaml:"args"`
Function string `yaml:"function"`
Shell string `yaml:"shell"`
Dir string `yaml:"dir"`
WorkingDir string `yaml:"working_dir"`
@@ -196,6 +197,7 @@ func normalizeStep(rs rawDagStep) (DagStep, error) {
Command: rs.Command,
Script: rs.Script,
Args: rs.Args,
Function: rs.Function,
Shell: rs.Shell,
Dir: dir,
Depends: rs.Depends,
+24
View File
@@ -187,6 +187,30 @@ steps:
}
})
t.Run("parsea step con function id y args", func(t *testing.T) {
data := []byte(`
name: dag-function
steps:
- name: foo
function: audit_capability_groups_go_infra
args:
- --json
`)
dag, err := DagParse(data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(dag.Steps) != 1 {
t.Fatalf("Steps: got %d, want 1", len(dag.Steps))
}
if dag.Steps[0].Function != "audit_capability_groups_go_infra" {
t.Errorf("Steps[0].Function: got %q, want %q", dag.Steps[0].Function, "audit_capability_groups_go_infra")
}
if len(dag.Steps[0].Args) != 1 || dag.Steps[0].Args[0] != "--json" {
t.Errorf("Steps[0].Args: got %v, want [--json]", dag.Steps[0].Args)
}
})
t.Run("parsea type graph", func(t *testing.T) {
data := []byte(`
name: dag-graph
+21 -1
View File
@@ -1,6 +1,13 @@
package core
import "fmt"
import (
"fmt"
"regexp"
)
// functionIDPattern matches registry IDs in `<name>_<lang>_<domain>` form.
// Compiled once and reused across validations.
var functionIDPattern = regexp.MustCompile(`^[a-z0-9_]+_[a-z]+_[a-z]+$`)
// DagValidate validates a DagDefinition for structural correctness.
// Checks: steps have name/ID, no duplicate names/IDs, all depends reference
@@ -31,6 +38,19 @@ func DagValidate(dag DagDefinition) DagValidationResult {
result.Warnings = append(result.Warnings,
fmt.Sprintf("step %q: has both command and script", ref))
}
// Function-step validation.
if step.Function != "" {
if !functionIDPattern.MatchString(step.Function) {
result.Errors = append(result.Errors,
fmt.Sprintf("step %s: invalid function id format: %s", ref, step.Function))
result.Valid = false
}
if step.Command != "" || step.Script != "" {
result.Warnings = append(result.Warnings,
fmt.Sprintf("step %s: function takes precedence; command/script ignored", ref))
}
}
}
if !result.Valid {
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: pure
signature: "func DagValidate(dag DagDefinition) DagValidationResult"
description: "Valida un DagDefinition para correcto uso estructural. Verifica que cada step tenga nombre o ID, que no haya duplicados, que todos los depends referencien steps existentes y que no haya ciclos (algoritmo de Kahn). Si el DAG es valido, calcula los niveles topologicos."
tags: [dag, validation, workflow, graph, pure]
tags: [dag, validation, workflow, graph, pure, validator]
uses_functions: [dag_topo_sort_go_core]
uses_types: [dag_definition_go_core, dag_validation_result_go_core]
returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: pure
signature: "func NextCronTime(schedule CronSchedule, after time.Time) time.Time"
description: "Calcula la proxima ejecucion de un cron schedule despues de un tiempo dado. Avanza minuto a minuto saltando campos no coincidentes. Retorna zero time si no hay match en 366 dias (schedule imposible)."
tags: [cron, scheduling, time, next, pure]
tags: [cron, scheduling, time, next, pure, scheduler]
uses_functions: [parse_cron_expr_go_core]
uses_types: [cron_schedule_go_core]
returns: []
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: pure
signature: "func ParseCronExpr(expr string) (CronSchedule, error)"
description: "Parsea una expresion cron estandar de 5 campos en un CronSchedule con valores expandidos. Soporta *, rangos (1-5), listas (1,3,5), pasos (*/15) y aliases (@hourly, @daily, @weekly, @monthly, @yearly). No soporta segundos ni years estilo Quartz."
tags: [cron, scheduling, parsing, time, pure]
tags: [cron, scheduling, parsing, time, pure, scheduler]
uses_functions: []
uses_types: [cron_schedule_go_core]
returns: []