package sdlc import ( "fmt" "os" "time" "gopkg.in/yaml.v3" ) // State represents the global SDLC state stored in .sdlc/state.yaml. type State struct { Version int `yaml:"version" json:"version"` Project ProjectState `yaml:"project" json:"project"` ActiveWork ActiveWork `yaml:"active_work" json:"active_work"` Blocked []BlockedItem `yaml:"blocked" json:"blocked"` LastUpdated *time.Time `yaml:"last_updated,omitempty" json:"last_updated,omitempty"` LastAction string `yaml:"last_action,omitempty" json:"last_action,omitempty"` LastActor string `yaml:"last_actor,omitempty" json:"last_actor,omitempty"` History []HistoryEntry `yaml:"history,omitempty" json:"history,omitempty"` } // LoadState reads and parses .sdlc/state.yaml from the given project root. func LoadState(root string) (*State, error) { path := StatePath(root) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, ErrNotInitialized } return nil, fmt.Errorf("read state file: %w", err) } var s State if err := yaml.Unmarshal(data, &s); err != nil { return nil, fmt.Errorf("parse state file: %w", err) } return &s, nil } // Save writes the state to .sdlc/state.yaml. func (s *State) Save(root string) error { now := time.Now().UTC() s.LastUpdated = &now data, err := yaml.Marshal(s) if err != nil { return fmt.Errorf("marshal state: %w", err) } path := StatePath(root) if err := os.WriteFile(path, data, 0o644); err != nil { return fmt.Errorf("write state file: %w", err) } return nil } // RecordAction appends a history entry and updates the last-action fields. // The result parameter allows recording success or failure status. func (s *State) RecordAction(action, feature, actor, result string) { if result == "" { result = "success" } entry := HistoryEntry{ Timestamp: time.Now().UTC(), Action: action, Feature: feature, Actor: actor, Result: result, } s.History = append(s.History, entry) s.LastAction = action s.LastActor = actor } // AddActiveFeature adds a feature to active work if not already present. func (s *State) AddActiveFeature(slug string, phase FeaturePhase) { for _, f := range s.ActiveWork.Features { if f.Slug == slug { return } } s.ActiveWork.Features = append(s.ActiveWork.Features, ActiveFeature{ Slug: slug, Phase: phase, }) } // UpdateActiveFeature updates the phase of an active feature. func (s *State) UpdateActiveFeature(slug string, phase FeaturePhase, branch string) { for i, f := range s.ActiveWork.Features { if f.Slug == slug { s.ActiveWork.Features[i].Phase = phase if branch != "" { s.ActiveWork.Features[i].Branch = branch } return } } } // RemoveActiveFeature removes a feature from active work. func (s *State) RemoveActiveFeature(slug string) { for i, f := range s.ActiveWork.Features { if f.Slug == slug { s.ActiveWork.Features = append(s.ActiveWork.Features[:i], s.ActiveWork.Features[i+1:]...) return } } } // DefaultState returns a new State with version 1 and empty fields. func DefaultState(projectName string) *State { return &State{ Version: 1, Project: ProjectState{ Name: projectName, }, ActiveWork: ActiveWork{ Features: []ActiveFeature{}, }, Blocked: []BlockedItem{}, History: []HistoryEntry{}, } }