// Package project 内的 phase_gate.go 实现"阶段状态机网关":把原来 ChangePhase // 的 any-to-any 替换为按目标 phase 声明 entry 网关的谓词引擎。 // // 设计取舍: // - 网关只在"流转进入"目标 phase 时运行,不校验已有状态——历史上已处于 // implementing/done 的需求(迁移前数据)天然被豁免,无需数据迁移。 // - 未声明网关的目标 phase(planning/prototyping/auditing/reviewing)保持 // any-to-any 旧行为(空网关列表 = 永远放行)。 // - 网关依赖通过 GateDeps 注入(Repo 查阶段指针、Workspace 查 PR 合并状态), // 便于单测用 fake 覆盖;Done 网关在 Workspace 缺失时按"未合并"优雅降级。 package project import ( "context" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" ) // PhaseGate 是单个 entry 谓词:返回 nil 表示 from->to 对 r 允许。 type PhaseGate interface { Check(ctx context.Context, dep GateDeps, r *Requirement, from, to Phase) error } // GateDeps 是网关运行所需的外部依赖。Workspace 可为 nil(Done 网关据此降级为 // "未合并",从而阻止流转——更安全的默认)。 type GateDeps struct { Repo Repository Workspace WorkspaceStateLookup } // phaseGates 是 target phase -> entry 网关列表的转换表。只有 implementing/done // 声明网关;其余 phase 列表为空(保持 any-to-any)。 // // implementing: 需要 auditing 阶段有 verdict=pass 的已批准产物(阶段指针就绪) // done: 需要 requirement 关联 workspace 且其 PR 已合并 var phaseGates = map[Phase][]PhaseGate{ PhaseImplementing: {requireApprovedArtifact(PhaseAuditing)}, PhaseDone: {requireWorkspaceMerged{}}, } // gateRunner 是 GateRunner 的默认实现:解析 to 的网关列表并逐个运行。 type gateRunner struct { gates map[Phase][]PhaseGate dep GateDeps } // NewGateRunner 用转换表与依赖构造 GateRunner。gates 传 nil 时用包内默认表。 func NewGateRunner(dep GateDeps, gates map[Phase][]PhaseGate) GateRunner { if gates == nil { gates = phaseGates } return &gateRunner{gates: gates, dep: dep} } // Check 运行目标 phase 的全部 entry 网关,遇首个失败即返回。 func (g *gateRunner) Check(ctx context.Context, r *Requirement, from, to Phase) error { for _, gate := range g.gates[to] { if err := gate.Check(ctx, g.dep, r, from, to); err != nil { return err } } return nil } // requireApprovedArtifact 网关:命名 phase 必须存在已批准(指针非空)且其产物 // verdict==pass 的产物。implementing 据此要求 auditing 报告先过审。 type requireApprovedArtifact Phase func (g requireApprovedArtifact) Check(ctx context.Context, dep GateDeps, r *Requirement, _, to Phase) error { gatedPhase := Phase(g) ptr, err := dep.Repo.GetPhasePointer(ctx, r.ID, gatedPhase) if err != nil { if ae, ok := errs.As(err); ok && ae.Code == errs.CodeNotFound { return errs.New(errs.CodePhaseGateFailed, "流转到 "+string(to)+" 前需先批准 "+string(gatedPhase)+" 阶段产物(verdict=pass)") } return err } if ptr.ApprovedArtifactID == nil { return errs.New(errs.CodePhaseGateFailed, "流转到 "+string(to)+" 前需先批准 "+string(gatedPhase)+" 阶段产物(verdict=pass)") } art, err := dep.Repo.GetArtifactByID(ctx, *ptr.ApprovedArtifactID) if err != nil { return err } if art.Verdict != VerdictPass { return errs.New(errs.CodePhaseGateFailed, "流转到 "+string(to)+" 前 "+string(gatedPhase)+" 阶段已批准产物的 verdict 必须为 pass") } return nil } // requireWorkspaceMerged 网关:requirement 必须关联 workspace,且该 workspace 的 // PR 已合并。Workspace 依赖缺失或查询失败时优雅降级为"未合并"(阻止流转)。 type requireWorkspaceMerged struct{} func (requireWorkspaceMerged) Check(ctx context.Context, dep GateDeps, r *Requirement, _, to Phase) error { if r.WorkspaceID == nil { return errs.New(errs.CodePhaseGateFailed, "流转到 "+string(to)+" 前需先关联 workspace 并合入其 PR") } if dep.Workspace == nil { return errs.New(errs.CodePhaseGateFailed, "无法确认 workspace PR 合并状态,暂不允许流转到 "+string(to)) } merged, err := dep.Workspace.PullRequestMerged(ctx, *r.WorkspaceID, r.ID) if err != nil { return errs.Wrap(err, errs.CodePhaseGateFailed, "查询 workspace PR 合并状态失败") } if !merged { return errs.New(errs.CodePhaseGateFailed, "流转到 "+string(to)+" 前需先合入 workspace 的 PR") } return nil }