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} }