This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+116
View File
@@ -0,0 +1,116 @@
// 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
}