rdev/internal/sdlc/classifier.go
jordan 425ef0f806 feat: add SDLC orchestration - library, CLI, and API integration
Implements deterministic feature lifecycle management for agent-driven
development. Agents use the CLI in pods; operators control via REST API.

Library (internal/sdlc/):
- Feature lifecycle with 10 phases (draft → released)
- Classifier engine with priority-ordered rules
- Artifact tracking with approval workflow
- Task management within features
- YAML-based state persistence

CLI (cmd/sdlc/):
- init, state, next, feature, artifact, task, query commands
- --json flag for machine-readable output
- Runs inside project pods

API (21 endpoints under /projects/{id}/sdlc/):
- State: GET /state, GET /next
- Features: CRUD + transition/block/unblock
- Artifacts: approve/reject per type
- Tasks: add/start/complete/block
- Queries: blocked/ready/needs-approval

Architecture:
- Port: SDLCExecutor interface (internal/port/)
- Adapter: kubectl exec into pods (internal/adapter/kubernetes/)
- Service: pod resolution + logging (internal/service/)
- Handlers: 5 files under 500-line limit (internal/handlers/)

Also includes template upgrades (chassis framework, UI components,
OpenAPI helpers, backend/frontend guides) and component improvements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 09:57:05 -07:00

92 lines
2.8 KiB
Go

package sdlc
import "time"
// Classification is the output of the classifier engine.
type Classification struct {
Timestamp time.Time `json:"timestamp" yaml:"timestamp"`
Feature string `json:"feature" yaml:"feature"`
CurrentPhase FeaturePhase `json:"current_phase" yaml:"current_phase"`
RuleMatched string `json:"rule_matched" yaml:"rule_matched"`
Action ActionType `json:"action" yaml:"action"`
Message string `json:"message" yaml:"message"`
NextCommand string `json:"next_command,omitempty" yaml:"next_command,omitempty"`
OutputPath string `json:"output_path,omitempty" yaml:"output_path,omitempty"`
TransitionTo FeaturePhase `json:"transition_to,omitempty" yaml:"transition_to,omitempty"`
TaskID string `json:"task_id,omitempty" yaml:"task_id,omitempty"`
}
// EvalContext provides all the state needed for rule evaluation.
type EvalContext struct {
State *State
Feature *Feature
Config *Config
Root string
}
// Rule is a single classifier rule with a condition and resulting action.
type Rule struct {
ID string
Condition func(ctx *EvalContext) bool
Action ActionType
Message func(ctx *EvalContext) string
NextCommand func(ctx *EvalContext) string
OutputPath func(ctx *EvalContext) string
TransitionTo FeaturePhase
TaskID func(ctx *EvalContext) string
}
// Classifier evaluates rules in priority order, returning the first match.
type Classifier struct {
rules []Rule
}
// NewClassifier creates a classifier with the default rules.
func NewClassifier() *Classifier {
return &Classifier{rules: DefaultRules()}
}
// NewClassifierWithRules creates a classifier with custom rules.
func NewClassifierWithRules(rules []Rule) *Classifier {
return &Classifier{rules: rules}
}
// Classify evaluates all rules against the context and returns the first match.
func (c *Classifier) Classify(ctx *EvalContext) *Classification {
for _, rule := range c.rules {
if rule.Condition(ctx) {
cl := &Classification{
Timestamp: time.Now().UTC(),
Feature: ctx.Feature.Slug,
CurrentPhase: ctx.Feature.Phase,
RuleMatched: rule.ID,
Action: rule.Action,
TransitionTo: rule.TransitionTo,
}
if rule.Message != nil {
cl.Message = rule.Message(ctx)
}
if rule.NextCommand != nil {
cl.NextCommand = rule.NextCommand(ctx)
}
if rule.OutputPath != nil {
cl.OutputPath = rule.OutputPath(ctx)
}
if rule.TaskID != nil {
cl.TaskID = rule.TaskID(ctx)
}
return cl
}
}
// Default: nothing to do
return &Classification{
Timestamp: time.Now().UTC(),
Feature: ctx.Feature.Slug,
CurrentPhase: ctx.Feature.Phase,
RuleMatched: "nothing-to-do",
Action: ActionIdle,
Message: "No actionable work found",
}
}