Major changes: - Add internal/logging package with field constants, context propagation, sensitive data auto-redaction, and per-component log levels - Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.) - Extend SDLC with callback handlers, generate endpoints, and executor - Add new cookbook trees for aeries and slackpath progression - Add skeleton templates for queue, realtime, and microservices - Add worker component template with async job processing - Refactor services and handlers to use new logging infrastructure - Split component.go into component_infra.go and component_listing.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
162 lines
4.4 KiB
Go
162 lines
4.4 KiB
Go
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
|
|
}
|