a03675113a
- .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>
104 lines
2.8 KiB
Go
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
|
|
}
|