Files
fn_registry/functions/core/dag_validate.go
T
egutierrez a03675113a 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>
2026-05-16 16:33:22 +02:00

104 lines
2.8 KiB
Go

package core
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
// existing steps, no dependency cycles. On success, computes topological levels.
// Returns warnings for steps with both command and script set.
func DagValidate(dag DagDefinition) DagValidationResult {
result := DagValidationResult{Valid: true}
// Build name/ID sets and check for missing identifiers and duplicates.
seen := make(map[string]bool)
for i, step := range dag.Steps {
ref := stepRef(step)
if ref == "" {
result.Errors = append(result.Errors,
fmt.Sprintf("step[%d]: must have name or id", i))
result.Valid = false
continue
}
if seen[ref] {
result.Errors = append(result.Errors,
fmt.Sprintf("step[%d]: duplicate name/id %q", i, ref))
result.Valid = false
}
seen[ref] = true
// Warning: command and script both set.
if step.Command != "" && step.Script != "" {
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 {
return result
}
// Check that all depends reference existing steps.
for _, step := range dag.Steps {
for _, dep := range step.Depends {
if !seen[dep] {
result.Errors = append(result.Errors,
fmt.Sprintf("step %q: depends on unknown step %q", stepRef(step), dep))
result.Valid = false
}
}
}
if !result.Valid {
return result
}
// Topological sort with Kahn's — detects cycles and computes levels.
levels, err := DagTopoSort(dag.Steps)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("cycle detected: %v", err))
result.Valid = false
return result
}
// Convert [][]DagStep to [][]string for the result.
strLevels := make([][]string, len(levels))
for i, level := range levels {
refs := make([]string, len(level))
for j, s := range level {
refs[j] = stepRef(s)
}
strLevels[i] = refs
}
result.Levels = strLevels
return result
}
// stepRef returns the canonical reference for a step (ID preferred, then Name).
func stepRef(s DagStep) string {
if s.ID != "" {
return s.ID
}
return s.Name
}