package service import ( "context" "fmt" "log/slog" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/sdlc" ) // SDLCService provides SDLC operations for projects. // It resolves project IDs to pod names and delegates to the SDLCExecutor. type SDLCService struct { sdlcExec port.SDLCExecutor projectRepo port.ProjectRepository logger *slog.Logger } // SDLCServiceConfig configures the SDLC service. type SDLCServiceConfig struct { Logger *slog.Logger } // NewSDLCService creates a new SDLC service. func NewSDLCService(sdlcExec port.SDLCExecutor, projectRepo port.ProjectRepository, cfg SDLCServiceConfig) *SDLCService { logger := cfg.Logger if logger == nil { logger = slog.Default() } return &SDLCService{ sdlcExec: sdlcExec, projectRepo: projectRepo, logger: logger.With("component", "sdlc-service"), } } // resolveProjectPod looks up a project and returns its pod name. func (s *SDLCService) resolveProjectPod(ctx context.Context, projectID string) (string, error) { project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID)) if err != nil { return "", domain.ErrProjectNotFound } if project.PodName == "" { return "", fmt.Errorf("project %s has no pod", projectID) } return project.PodName, nil } // GetState returns the global SDLC state for a project. func (s *SDLCService) GetState(ctx context.Context, projectID string) (*sdlc.State, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.GetState(ctx, podName) } // GetNext returns the classifier's recommendation for the next action. func (s *SDLCService) GetNext(ctx context.Context, projectID, feature string) (*sdlc.Classification, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.GetNext(ctx, podName, feature) } // ListFeatures returns all features in a project. func (s *SDLCService) ListFeatures(ctx context.Context, projectID string) ([]*sdlc.Feature, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.ListFeatures(ctx, podName) } // GetFeature returns a single feature by slug. func (s *SDLCService) GetFeature(ctx context.Context, projectID, slug string) (*sdlc.Feature, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.GetFeature(ctx, podName, slug) } // CreateFeature creates a new feature. func (s *SDLCService) CreateFeature(ctx context.Context, projectID, slug, title string) (*sdlc.Feature, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } f, err := s.sdlcExec.CreateFeature(ctx, podName, slug, title) if err != nil { return nil, err } s.logger.Info("feature created", "project", projectID, "feature", slug) return f, nil } // TransitionFeature moves a feature to the specified phase. func (s *SDLCService) TransitionFeature(ctx context.Context, projectID, slug string, phase sdlc.FeaturePhase) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.TransitionFeature(ctx, podName, slug, phase); err != nil { s.logger.Error("transition feature failed", "project", projectID, "feature", slug, "phase", string(phase), "error", err) return err } s.logger.Info("feature transitioned", "project", projectID, "feature", slug, "phase", string(phase)) return nil } // BlockFeature adds a blocker reason to a feature. func (s *SDLCService) BlockFeature(ctx context.Context, projectID, slug, reason string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.BlockFeature(ctx, podName, slug, reason); err != nil { return err } s.logger.Info("feature blocked", "project", projectID, "feature", slug, "reason", reason) return nil } // UnblockFeature removes all blockers from a feature. func (s *SDLCService) UnblockFeature(ctx context.Context, projectID, slug string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.UnblockFeature(ctx, podName, slug); err != nil { return err } s.logger.Info("feature unblocked", "project", projectID, "feature", slug) return nil } // DeleteFeature removes a feature entirely. func (s *SDLCService) DeleteFeature(ctx context.Context, projectID, slug string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.DeleteFeature(ctx, podName, slug); err != nil { return err } s.logger.Info("feature deleted", "project", projectID, "feature", slug) return nil } // GetArtifactStatus returns artifact statuses for a feature. func (s *SDLCService) GetArtifactStatus(ctx context.Context, projectID, slug string) (map[sdlc.ArtifactType]*sdlc.Artifact, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.GetArtifactStatus(ctx, podName, slug) } // ApproveArtifact approves a feature artifact. func (s *SDLCService) ApproveArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.ApproveArtifact(ctx, podName, slug, artType); err != nil { return err } s.logger.Info("artifact approved", "project", projectID, "feature", slug, "artifact", string(artType)) return nil } // RejectArtifact rejects a feature artifact. func (s *SDLCService) RejectArtifact(ctx context.Context, projectID, slug string, artType sdlc.ArtifactType) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.RejectArtifact(ctx, podName, slug, artType); err != nil { return err } s.logger.Info("artifact rejected", "project", projectID, "feature", slug, "artifact", string(artType)) return nil } // ListTasks returns all tasks for a feature. func (s *SDLCService) ListTasks(ctx context.Context, projectID, slug string) ([]sdlc.Task, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.ListTasks(ctx, podName, slug) } // AddTask adds a new task to a feature. func (s *SDLCService) AddTask(ctx context.Context, projectID, slug, title string) (*sdlc.Task, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.AddTask(ctx, podName, slug, title) } // StartTask marks a task as in-progress. func (s *SDLCService) StartTask(ctx context.Context, projectID, slug, taskID string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } return s.sdlcExec.StartTask(ctx, podName, slug, taskID) } // CompleteTask marks a task as complete. func (s *SDLCService) CompleteTask(ctx context.Context, projectID, slug, taskID string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } return s.sdlcExec.CompleteTask(ctx, podName, slug, taskID) } // BlockTask marks a task as blocked. func (s *SDLCService) BlockTask(ctx context.Context, projectID, slug, taskID string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } return s.sdlcExec.BlockTask(ctx, podName, slug, taskID) } // QueryBlocked returns all blocked features in a project. func (s *SDLCService) QueryBlocked(ctx context.Context, projectID string) ([]port.BlockedInfo, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.QueryBlocked(ctx, podName) } // QueryReady returns features ready for work in a project. func (s *SDLCService) QueryReady(ctx context.Context, projectID string) ([]port.ReadyInfo, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.QueryReady(ctx, podName) } // QueryNeedsApproval returns features awaiting approval in a project. func (s *SDLCService) QueryNeedsApproval(ctx context.Context, projectID string) ([]port.ApprovalInfo, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.QueryNeedsApproval(ctx, podName) } // CreateBranch creates a feature branch and its manifest. func (s *SDLCService) CreateBranch(ctx context.Context, projectID, slug string) (*sdlc.BranchManifest, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } m, err := s.sdlcExec.CreateBranch(ctx, podName, slug) if err != nil { return nil, err } s.logger.Info("branch created", "project", projectID, "feature", slug, "branch", m.Name) return m, nil } // GetBranchStatus returns the full branch status including checklist. func (s *SDLCService) GetBranchStatus(ctx context.Context, projectID, slug string) (*port.BranchStatus, error) { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return nil, err } return s.sdlcExec.GetBranchStatus(ctx, podName, slug) } // SyncBranch syncs a feature branch with its base branch. func (s *SDLCService) SyncBranch(ctx context.Context, projectID, slug string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.SyncBranch(ctx, podName, slug); err != nil { return err } s.logger.Info("branch synced", "project", projectID, "feature", slug) return nil } // MergeFeature merges a feature branch after all gates pass. func (s *SDLCService) MergeFeature(ctx context.Context, projectID, slug, strategy string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.MergeFeature(ctx, podName, slug, strategy); err != nil { s.logger.Error("merge feature failed", "project", projectID, "feature", slug, "error", err) return err } s.logger.Info("feature merged", "project", projectID, "feature", slug, "strategy", strategy) return nil } // ArchiveFeature archives a released feature. func (s *SDLCService) ArchiveFeature(ctx context.Context, projectID, slug string) error { podName, err := s.resolveProjectPod(ctx, projectID) if err != nil { return err } if err := s.sdlcExec.ArchiveFeature(ctx, podName, slug); err != nil { return err } s.logger.Info("feature archived", "project", projectID, "feature", slug) return nil }