Keep registry key validation local to workflowdef with composition-root injection, adapt FlatCatalog for projectworkflow, and drive the orchestrator through LiveInstancePort views instead of projectinstance.
93 lines
3.5 KiB
Go
93 lines
3.5 KiB
Go
package workflowdef
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type templateReadyChecker func(assetKey string, version int) bool
|
|
type upstreamExistsChecker func(workflowKey string) bool
|
|
|
|
// ValidateDefinition 对工作流版本定义做确定性校验。
|
|
// execValidate / gateValidate are optional registry ports (nil skips).
|
|
func ValidateDefinition(def VersionDefinition, tplReady templateReadyChecker, upstreamExists upstreamExistsChecker, execValidate, gateValidate keyValidator) ValidationResult {
|
|
if def.SchemaVersion >= 2 {
|
|
return ValidateDefinitionV2(def, execValidate, gateValidate)
|
|
}
|
|
issues := make([]ValidationIssue, 0)
|
|
if def.SchemaVersion <= 0 {
|
|
def.SchemaVersion = 1
|
|
}
|
|
if len(def.Stages) == 0 {
|
|
issues = append(issues, ValidationIssue{Code: "NO_STAGES", Message: "至少需要一个阶段"})
|
|
return ValidationResult{Valid: false, Issues: issues}
|
|
}
|
|
|
|
stageKeys := map[string]struct{}{}
|
|
taskKeys := map[string]struct{}{}
|
|
enabledStageCount := 0
|
|
|
|
sorted := append([]StageDef(nil), def.Stages...)
|
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Position < sorted[j].Position })
|
|
|
|
for i, st := range sorted {
|
|
if st.Key == "" {
|
|
issues = append(issues, ValidationIssue{Code: "STAGE_KEY_REQUIRED", Message: "阶段 key 不能为空", Field: fmt.Sprintf("stages[%d].key", i)})
|
|
continue
|
|
}
|
|
if _, dup := stageKeys[st.Key]; dup {
|
|
issues = append(issues, ValidationIssue{Code: "DUPLICATE_STAGE_KEY", Message: fmt.Sprintf("重复阶段 key: %s", st.Key), Field: st.Key})
|
|
}
|
|
stageKeys[st.Key] = struct{}{}
|
|
if st.Position != i {
|
|
issues = append(issues, ValidationIssue{Code: "STAGE_POSITION_GAP", Message: fmt.Sprintf("阶段 %s 顺序应为 %d", st.Key, i), Field: st.Key})
|
|
}
|
|
|
|
enabledTasks := 0
|
|
for j, dt := range st.DocTasks {
|
|
if dt.Key == "" {
|
|
issues = append(issues, ValidationIssue{Code: "TASK_KEY_REQUIRED", Message: "文档任务 key 不能为空", Field: fmt.Sprintf("%s.doc_tasks[%d]", st.Key, j)})
|
|
continue
|
|
}
|
|
if _, dup := taskKeys[dt.Key]; dup {
|
|
issues = append(issues, ValidationIssue{Code: "DUPLICATE_TASK_KEY", Message: fmt.Sprintf("重复任务 key: %s", dt.Key), Field: dt.Key})
|
|
}
|
|
taskKeys[dt.Key] = struct{}{}
|
|
if dt.OutputDocType == "" || dt.OutputDocTitle == "" {
|
|
issues = append(issues, ValidationIssue{Code: "TASK_OUTPUT_REQUIRED", Message: fmt.Sprintf("任务 %s 需 output_doc_type/title", dt.Key), Field: dt.Key})
|
|
}
|
|
if dt.DefaultEnabled {
|
|
enabledTasks++
|
|
}
|
|
if dt.TemplateAssetKey != "" && tplReady != nil {
|
|
ver := dt.TemplateVersion
|
|
if ver <= 0 {
|
|
ver = 1
|
|
}
|
|
if !tplReady(dt.TemplateAssetKey, ver) {
|
|
issues = append(issues, ValidationIssue{Code: "TEMPLATE_NOT_READY", Message: fmt.Sprintf("模板 %s v%d 不可用", dt.TemplateAssetKey, ver), Field: dt.Key})
|
|
}
|
|
}
|
|
}
|
|
if !st.Optional {
|
|
if enabledTasks == 0 && !st.ManualGate {
|
|
issues = append(issues, ValidationIssue{Code: "STAGE_EMPTY", Message: fmt.Sprintf("阶段 %s 无启用文档且未设 manual_gate", st.Key), Field: st.Key})
|
|
} else {
|
|
enabledStageCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
if enabledStageCount == 0 {
|
|
issues = append(issues, ValidationIssue{Code: "NO_ENABLED_STAGE", Message: "至少一个非 optional 阶段需有启用文档或 manual_gate"})
|
|
}
|
|
|
|
up := strings.TrimSpace(def.RequiredUpstreamWorkflow)
|
|
if up != "" && upstreamExists != nil && !upstreamExists(up) {
|
|
issues = append(issues, ValidationIssue{Code: "UPSTREAM_NOT_FOUND", Message: fmt.Sprintf("上游工作流 %s 不存在", up), Field: "required_upstream_workflow"})
|
|
}
|
|
|
|
return ValidationResult{Valid: len(issues) == 0, Issues: issues}
|
|
}
|