package core import ( "fmt" "regexp" ) // functionIDPattern matches registry IDs in `__` 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 }