- Add auth.RequireScope() to all handler routes for proper authorization - Add SDLC OpenAPI endpoint documentation (state, features, tasks, branches, merge, archive, orchestrator) - Add SDLC documentation guides (getting-started, cli-reference, api-reference, command-catalog) - Add artifact_test.go for SDLC artifact coverage - Add CLAUDE.md rules: auth scopes requirement, error wrapping with %w - Fix error wrapping to use %w instead of %v throughout codebase - Improve CLI merge command with conflict detection and resolution - Fix handler tests to include auth middleware for RequireScope - Add cookbook tree runner scripts for automated testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
145 lines
3.6 KiB
Go
145 lines
3.6 KiB
Go
package sdlc
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Task represents an implementation task within a feature.
|
|
type Task struct {
|
|
ID string `yaml:"id" json:"id"`
|
|
Title string `yaml:"title" json:"title"`
|
|
Status TaskStatus `yaml:"status" json:"status"`
|
|
Spec string `yaml:"spec,omitempty" json:"spec,omitempty"`
|
|
Files []string `yaml:"files,omitempty" json:"files,omitempty"`
|
|
Patterns []string `yaml:"patterns,omitempty" json:"patterns,omitempty"`
|
|
DependsOn []string `yaml:"depends_on,omitempty" json:"depends_on,omitempty"`
|
|
StartedAt *time.Time `yaml:"started_at,omitempty" json:"started_at,omitempty"`
|
|
DoneAt *time.Time `yaml:"done_at,omitempty" json:"done_at,omitempty"`
|
|
Notes string `yaml:"notes,omitempty" json:"notes,omitempty"`
|
|
}
|
|
|
|
// StartTask marks a task as in-progress.
|
|
func StartTask(tasks []Task, taskID string) ([]Task, error) {
|
|
for i, t := range tasks {
|
|
if t.ID == taskID {
|
|
if t.Status != TaskPending && t.Status != TaskBlocked {
|
|
return tasks, fmt.Errorf("task %s is %s, not startable", taskID, t.Status)
|
|
}
|
|
now := time.Now().UTC()
|
|
tasks[i].Status = TaskInProgress
|
|
tasks[i].StartedAt = &now
|
|
return tasks, nil
|
|
}
|
|
}
|
|
return tasks, ErrTaskNotFound
|
|
}
|
|
|
|
// CompleteTask marks a task as complete.
|
|
func CompleteTask(tasks []Task, taskID string) ([]Task, error) {
|
|
for i, t := range tasks {
|
|
if t.ID == taskID {
|
|
if t.Status != TaskInProgress {
|
|
return tasks, fmt.Errorf("task %s is %s, not completable", taskID, t.Status)
|
|
}
|
|
now := time.Now().UTC()
|
|
tasks[i].Status = TaskComplete
|
|
tasks[i].DoneAt = &now
|
|
return tasks, nil
|
|
}
|
|
}
|
|
return tasks, ErrTaskNotFound
|
|
}
|
|
|
|
// BlockTask marks a task as blocked.
|
|
func BlockTask(tasks []Task, taskID string) ([]Task, error) {
|
|
for i, t := range tasks {
|
|
if t.ID == taskID {
|
|
tasks[i].Status = TaskBlocked
|
|
return tasks, nil
|
|
}
|
|
}
|
|
return tasks, ErrTaskNotFound
|
|
}
|
|
|
|
// AddTask appends a new task with an auto-generated ID.
|
|
// The ID is based on the max existing ID + 1 to avoid collisions after deletions.
|
|
func AddTask(tasks []Task, title string) []Task {
|
|
maxNum := 0
|
|
for _, t := range tasks {
|
|
var num int
|
|
if _, err := fmt.Sscanf(t.ID, "task-%d", &num); err == nil {
|
|
if num > maxNum {
|
|
maxNum = num
|
|
}
|
|
}
|
|
}
|
|
id := fmt.Sprintf("task-%03d", maxNum+1)
|
|
return append(tasks, Task{
|
|
ID: id,
|
|
Title: title,
|
|
Status: TaskPending,
|
|
})
|
|
}
|
|
|
|
// PendingTasks returns tasks that are pending or blocked.
|
|
func PendingTasks(tasks []Task) []Task {
|
|
var result []Task
|
|
for _, t := range tasks {
|
|
if t.Status == TaskPending {
|
|
result = append(result, t)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// NextTask returns the first pending task, or nil if none.
|
|
func NextTask(tasks []Task) *Task {
|
|
for i, t := range tasks {
|
|
if t.Status == TaskPending {
|
|
return &tasks[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AllTasksComplete returns true if every task is in the complete state.
|
|
func AllTasksComplete(tasks []Task) bool {
|
|
if len(tasks) == 0 {
|
|
return false
|
|
}
|
|
for _, t := range tasks {
|
|
if t.Status != TaskComplete {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TaskSummary returns counts by status.
|
|
type TaskSummary struct {
|
|
Total int `json:"total"`
|
|
Completed int `json:"completed"`
|
|
InProgress int `json:"in_progress"`
|
|
Pending int `json:"pending"`
|
|
Blocked int `json:"blocked"`
|
|
}
|
|
|
|
// SummarizeTasks computes a TaskSummary from a task list.
|
|
func SummarizeTasks(tasks []Task) TaskSummary {
|
|
s := TaskSummary{Total: len(tasks)}
|
|
for _, t := range tasks {
|
|
switch t.Status {
|
|
case TaskComplete:
|
|
s.Completed++
|
|
case TaskInProgress:
|
|
s.InProgress++
|
|
case TaskPending:
|
|
s.Pending++
|
|
case TaskBlocked:
|
|
s.Blocked++
|
|
}
|
|
}
|
|
return s
|
|
}
|