You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user