package service import ( "context" "database/sql" "fmt" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" ) // SDLCGenerateService handles async artifact generation via the work queue. type SDLCGenerateService struct { sdlcService *SDLCService buildService *BuildService db *sql.DB baseURL string } // SDLCGenerateServiceConfig configures the generate service. type SDLCGenerateServiceConfig struct { // BaseURL is the API base URL for callback URLs (e.g., "http://localhost:8080") BaseURL string } // NewSDLCGenerateService creates a new SDLC generate service. func NewSDLCGenerateService( sdlcService *SDLCService, buildService *BuildService, db *sql.DB, cfg SDLCGenerateServiceConfig, ) *SDLCGenerateService { return &SDLCGenerateService{ sdlcService: sdlcService, buildService: buildService, db: db, baseURL: cfg.BaseURL, } } // GenerateArtifact enqueues a build task to generate an SDLC artifact. func (s *SDLCGenerateService) GenerateArtifact( ctx context.Context, projectID, featureSlug string, req *domain.SDLCGenerateRequest, ) (*domain.SDLCGenerateResponse, error) { // Validate artifact type if !domain.IsValidGenerateArtifactType(req.ArtifactType) { return nil, fmt.Errorf("invalid artifact_type: %s", req.ArtifactType) } // Validate task_id is provided for code generation if req.ArtifactType == "code" && req.TaskID == "" { return nil, fmt.Errorf("task_id is required for artifact_type: code") } // Validate feature exists _, err := s.sdlcService.GetFeature(ctx, projectID, featureSlug) if err != nil { return nil, fmt.Errorf("get feature: %w", err) } // Get project git URL from database gitCloneURL, err := s.getProjectGitURL(ctx, projectID) if err != nil { return nil, fmt.Errorf("get project git URL: %w", err) } // Build the prompt from artifact type prompt := s.buildPrompt(featureSlug, req.ArtifactType, req.TaskID) // Build callback URL for SDLC status updates callbackURL := s.baseURL + "/internal/sdlc/callback" // Create build spec with SDLC context buildSpec := domain.BuildSpec{ Prompt: prompt, AutoCommit: true, AutoPush: true, GitCloneURL: gitCloneURL, CallbackURL: callbackURL, } // Build SDLC context map for callback routing sdlcContext := map[string]any{ "feature": featureSlug, "artifact_type": req.ArtifactType, } if req.TaskID != "" { sdlcContext["task_id"] = req.TaskID } taskID, err := s.buildService.StartBuildWithSDLCContext(ctx, projectID, buildSpec, sdlcContext) if err != nil { return nil, fmt.Errorf("start build: %w", err) } log := logging.FromContext(ctx).WithService("sdlc_generate") log.Info("artifact generation enqueued", logging.FieldProjectID, projectID, "feature", featureSlug, "artifact_type", req.ArtifactType, "task_id", taskID, ) return &domain.SDLCGenerateResponse{ TaskID: taskID, ProjectID: projectID, Feature: featureSlug, ArtifactType: req.ArtifactType, Status: "pending", StatusURL: "/builds/" + taskID, StreamURL: fmt.Sprintf("/projects/%s/events?stream_id=%s", projectID, taskID), }, nil } // buildPrompt constructs the appropriate skeleton command for the artifact type. func (s *SDLCGenerateService) buildPrompt(feature, artifactType, taskID string) string { switch artifactType { case "spec": return "/spec-feature " + feature case "design": return "/design-feature " + feature case "tasks": return "/breakdown-feature " + feature case "code": if taskID != "" { return "/implement-task " + feature + " " + taskID } return "/implement-feature " + feature case "qa": return "/run-qa " + feature default: return fmt.Sprintf("Generate %s artifact for feature %s", artifactType, feature) } } // getProjectGitURL retrieves the git clone URL for a project from the database. func (s *SDLCGenerateService) getProjectGitURL(ctx context.Context, projectID string) (string, error) { if s.db == nil { return "", fmt.Errorf("database not configured") } var gitCloneHTTP sql.NullString err := s.db.QueryRowContext(ctx, `SELECT git_clone_http FROM projects WHERE id = $1`, projectID, ).Scan(&gitCloneHTTP) if err != nil { if err == sql.ErrNoRows { return "", domain.ErrProjectNotFound } return "", fmt.Errorf("query project: %w", err) } if !gitCloneHTTP.Valid || gitCloneHTTP.String == "" { return "", fmt.Errorf("project %s has no git URL configured", projectID) } return gitCloneHTTP.String, nil }