Files
weichenandCursor 90f77eac93 feat(authz): implement six-phase permissions, settings and workflow v2
Close P0 auth gaps for lineage/runs, stage ABAC fail-closed, and
project:create; add IAM migrations 00021-23, me settings APIs, schema v2
manifests, orchestrator skeleton, and gate A regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:23 +00:00

92 lines
3.4 KiB
Go

package workflowdef
import (
"fmt"
"sort"
"strings"
)
type templateReadyChecker func(assetKey string, version int) bool
type upstreamExistsChecker func(workflowKey string) bool
// ValidateDefinition 对工作流版本定义做确定性校验。
func ValidateDefinition(def VersionDefinition, tplReady templateReadyChecker, upstreamExists upstreamExistsChecker) ValidationResult {
if def.SchemaVersion >= 2 {
return ValidateDefinitionV2(def)
}
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}
}