package workflowdef import ( "fmt" ) var officialDocCounts = map[string]int{ OfficialKeyMain: 7, OfficialKeyProductManager: 1, OfficialKeyTestManager: 1, OfficialKeyRDev: 60, } // keyValidator checks a registry key (executor / quality gate). Injected so workflowdef // does not import the concrete workflow registry package. type keyValidator func(key string) error // ValidateDefinitionV2 schema v2 发布校验。 // execValidate / gateValidate are optional ports; nil uses the local registry snapshot // (so workflowdef never imports internal/workflow). func ValidateDefinitionV2(def VersionDefinition, execValidate, gateValidate keyValidator) ValidationResult { if execValidate == nil { execValidate = defaultValidateExecutorKey } if gateValidate == nil { gateValidate = defaultValidateQualityGateKey } issues := make([]ValidationIssue, 0) if def.SchemaVersion < 2 { issues = append(issues, ValidationIssue{Code: "SCHEMA_VERSION", Message: "schema_version must be >= 2"}) } if def.RequiredUpstreamWorkflow != "" { issues = append(issues, ValidationIssue{ Code: "UPSTREAM_NOT_ALLOWED", Message: "正式工作流不得声明 required_upstream_workflow", }) } seenPos := map[int]bool{} seenTask := map[string]bool{} docCount := 0 for _, st := range def.Stages { if st.Key == "" { issues = append(issues, ValidationIssue{Code: "STAGE_KEY_REQUIRED", Message: "stage key required"}) continue } if st.Position <= 0 { issues = append(issues, ValidationIssue{Code: "STAGE_POSITION", Field: st.Key, Message: "position must be > 0"}) } if seenPos[st.Position] { issues = append(issues, ValidationIssue{Code: "DUPLICATE_POSITION", Field: st.Key, Message: "duplicate stage position"}) } seenPos[st.Position] = true if len(st.DocTasks) == 0 { issues = append(issues, ValidationIssue{Code: "STAGE_NO_DOC_TASKS", Field: st.Key, Message: "each stage must have at least one doc task"}) } for _, dt := range st.DocTasks { docCount++ if dt.Key == "" { issues = append(issues, ValidationIssue{Code: "DOC_TASK_KEY_REQUIRED", Message: "doc task key required"}) continue } if seenTask[dt.Key] { issues = append(issues, ValidationIssue{Code: "DUPLICATE_DOC_TASK", Field: dt.Key, Message: "duplicate doc task key"}) } seenTask[dt.Key] = true if dt.ExecutorKey != "" { if err := execValidate(dt.ExecutorKey); err != nil { issues = append(issues, ValidationIssue{Code: "UNKNOWN_EXECUTOR", Field: dt.Key, Message: err.Error()}) } } if dt.QualityGateKey != "" { if err := gateValidate(dt.QualityGateKey); err != nil { issues = append(issues, ValidationIssue{Code: "UNKNOWN_QUALITY_GATE", Field: dt.Key, Message: err.Error()}) } } if dt.OutputDocType == "" { issues = append(issues, ValidationIssue{Code: "OUTPUT_DOC_TYPE", Field: dt.Key, Message: "output_doc_type required"}) } } } if len(def.Stages) == 0 { issues = append(issues, ValidationIssue{Code: "NO_STAGES", Message: "at least one stage required"}) } // 固定文档数约束医疗正式工作流;软件工作流按自身 manifest 完整性校验。 if def.WorkflowKey != "" && IsMedicalWorkflowKey(def.WorkflowKey) { if want, ok := officialDocCounts[def.WorkflowKey]; ok && docCount != want { issues = append(issues, ValidationIssue{ Code: "OFFICIAL_DOC_COUNT", Field: def.WorkflowKey, Message: fmt.Sprintf("official workflow %s requires exactly %d doc tasks, got %d", def.WorkflowKey, want, docCount), }) } } return ValidationResult{Valid: len(issues) == 0, Issues: issues} } // ValidateVersionBySchema 按 schema_version 路由校验器。 func ValidateVersionBySchema(def VersionDefinition, execValidate, gateValidate keyValidator) ValidationResult { if def.SchemaVersion >= 2 { return ValidateDefinitionV2(def, execValidate, gateValidate) } return validateDefinitionV1(def) } func validateDefinitionV1(def VersionDefinition) ValidationResult { issues := make([]ValidationIssue, 0) for i, st := range def.Stages { if st.Position != i { issues = append(issues, ValidationIssue{ Code: "STAGE_POSITION", Field: st.Key, Message: fmt.Sprintf("position must equal index %d", i), }) } } return ValidationResult{Valid: len(issues) == 0, Issues: issues} }