rdev/internal/sdlc/rules.go
jordan f22b220c6d feat: add SDLC branch management, merge, archive, and orchestrator APIs
Add branch lifecycle commands (branch, merge, archive) to the SDLC CLI.
Introduce orchestrator handler and service for multi-step SDLC workflows.
Expand skeleton template with 15 Claude commands covering the full feature
lifecycle. Extend classifier rules, error types, and executor port for
branch operations. Split rules.go and classifier_test.go to stay within
500-line limit.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:30:03 -07:00

278 lines
6.9 KiB
Go

package sdlc
import (
"fmt"
"strings"
)
// DefaultRules returns all classifier rules in priority order.
// First matching rule wins.
func DefaultRules() []Rule {
return []Rule{
blockedDependencyRule(),
needsSpecRule(),
specNeedsApprovalRule(),
specApprovedRule(),
needsDesignRule(),
designNeedsApprovalRule(),
needsTasksRule(),
tasksNeedApprovalRule(),
needsQAPlanRule(),
qaPlanNeedsApprovalRule(),
planningCompleteRule(),
needsBranchRule(),
readyToImplementRule(),
implementNextTaskRule(),
implementationCompleteRule(),
needsReviewRule(),
reviewHasIssuesRule(),
reviewPassedRule(),
needsAuditRule(),
auditHasIssuesRule(),
auditPassedRule(),
needsQARule(),
qaHasFailuresRule(),
qaPassedRule(),
needsMergeRule(),
archiveFeatureRule(),
}
}
func blockedDependencyRule() Rule {
return Rule{
ID: "blocked-dependency",
Condition: func(ctx *EvalContext) bool {
return ctx.Feature.IsBlocked()
},
Action: ActionBlocked,
Message: func(ctx *EvalContext) string {
return fmt.Sprintf("Feature blocked by: %s", strings.Join(ctx.Feature.Blockers, ", "))
},
}
}
func needsSpecRule() Rule {
return Rule{
ID: "needs-spec",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseDraft {
return false
}
art := ctx.Feature.GetArtifact(ArtifactSpec)
return art == nil || art.Status == StatusPending
},
Action: ActionCreateSpec,
NextCommand: func(ctx *EvalContext) string {
return "/spec-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/spec.md"
},
}
}
func specNeedsApprovalRule() Rule {
return Rule{
ID: "spec-needs-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseDraft {
return false
}
art := ctx.Feature.GetArtifact(ArtifactSpec)
return art != nil && art.Status == StatusDraft
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "Specification requires user approval"
},
}
}
func specApprovedRule() Rule {
return Rule{
ID: "spec-approved",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseDraft {
return false
}
art := ctx.Feature.GetArtifact(ArtifactSpec)
return art != nil && art.Status == StatusApproved
},
Action: ActionTransition,
TransitionTo: PhaseSpecified,
}
}
func needsDesignRule() Rule {
return Rule{
ID: "needs-design",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
art := ctx.Feature.GetArtifact(ArtifactDesign)
return art == nil || art.Status == StatusPending
},
Action: ActionCreateDesign,
NextCommand: func(ctx *EvalContext) string {
return "/design-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/design.md"
},
}
}
func designNeedsApprovalRule() Rule {
return Rule{
ID: "design-needs-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
art := ctx.Feature.GetArtifact(ArtifactDesign)
return art != nil && (art.Status == StatusDraft || art.Status == StatusRejected)
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "Design document requires user approval"
},
}
}
func needsTasksRule() Rule {
return Rule{
ID: "needs-tasks",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
design := ctx.Feature.GetArtifact(ArtifactDesign)
if design == nil || design.Status != StatusApproved {
return false
}
tasks := ctx.Feature.GetArtifact(ArtifactTasks)
return tasks == nil || tasks.Status == StatusPending
},
Action: ActionCreateTasks,
NextCommand: func(ctx *EvalContext) string {
return "/breakdown-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/tasks.md"
},
}
}
func tasksNeedApprovalRule() Rule {
return Rule{
ID: "tasks-need-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
tasks := ctx.Feature.GetArtifact(ArtifactTasks)
return tasks != nil && (tasks.Status == StatusDraft || tasks.Status == StatusRejected)
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "Task breakdown requires user approval"
},
}
}
func needsQAPlanRule() Rule {
return Rule{
ID: "needs-qa-plan",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
tasks := ctx.Feature.GetArtifact(ArtifactTasks)
if tasks == nil || tasks.Status != StatusApproved {
return false
}
qa := ctx.Feature.GetArtifact(ArtifactQAPlan)
return qa == nil || qa.Status == StatusPending
},
Action: ActionCreateQAPlan,
NextCommand: func(ctx *EvalContext) string {
return "/create-qa-plan " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/qa-plan.md"
},
}
}
func qaPlanNeedsApprovalRule() Rule {
return Rule{
ID: "qa-plan-needs-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
qa := ctx.Feature.GetArtifact(ArtifactQAPlan)
return qa != nil && (qa.Status == StatusDraft || qa.Status == StatusRejected)
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "QA plan requires user approval"
},
}
}
func planningCompleteRule() Rule {
return Rule{
ID: "planning-complete",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
qa := ctx.Feature.GetArtifact(ArtifactQAPlan)
return qa != nil && qa.Status == StatusApproved
},
Action: ActionTransition,
TransitionTo: PhasePlanned,
}
}
func needsBranchRule() Rule {
return Rule{
ID: "needs-branch",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhasePlanned {
return false
}
if ctx.Config == nil || !ctx.Config.Compliance.RequireBranch {
return false
}
return ctx.Feature.Branch == ""
},
Action: ActionCreateBranch,
Message: func(_ *EvalContext) string {
return "Feature branch required before implementation"
},
NextCommand: func(ctx *EvalContext) string {
return "sdlc branch create " + ctx.Feature.Slug
},
}
}
func readyToImplementRule() Rule {
return Rule{
ID: "ready-to-implement",
Condition: func(ctx *EvalContext) bool {
return ctx.Feature.Phase == PhasePlanned || ctx.Feature.Phase == PhaseReady
},
Action: ActionTransition,
TransitionTo: PhaseReady,
Message: func(ctx *EvalContext) string {
if ctx.Feature.Phase == PhasePlanned {
return "Ready for implementation: transition to ready, then implementation"
}
return "Ready for implementation"
},
}
}