You've already forked agentic-coding-workflow
117 lines
4.5 KiB
Go
117 lines
4.5 KiB
Go
// 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
|
|
}
|