// Package service provides business logic services. package service import ( "context" "fmt" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" ) // BuildService orchestrates build task submission and tracking. // It coordinates between the work queue (execution) and build audit (history). type BuildService struct { queue port.WorkQueue audit port.BuildAudit } // NewBuildService creates a new build service. func NewBuildService( queue port.WorkQueue, audit port.BuildAudit, ) *BuildService { return &BuildService{ queue: queue, audit: audit, } } // StartBuild enqueues a build task and creates an audit entry. // Returns the task ID for status tracking. func (s *BuildService) StartBuild(ctx context.Context, projectID string, spec domain.BuildSpec) (string, error) { if err := spec.Validate(); err != nil { return "", err } if projectID == "" { return "", fmt.Errorf("project_id is required") } // Build work task spec from build spec taskSpec := map[string]any{ "prompt": spec.Prompt, "auto_commit": spec.AutoCommit, "auto_push": spec.AutoPush, } if spec.Template != "" { taskSpec["template"] = spec.Template } if len(spec.Variables) > 0 { taskSpec["variables"] = spec.Variables } if spec.GitCloneURL != "" { taskSpec["git_clone_url"] = spec.GitCloneURL } // Create work task task := &domain.WorkTask{ ProjectID: projectID, Type: domain.WorkTaskTypeBuild, Spec: taskSpec, CallbackURL: spec.CallbackURL, MaxRetries: 3, } // Enqueue to work queue taskID, err := s.queue.Enqueue(ctx, task) if err != nil { return "", fmt.Errorf("enqueue build task: %w", err) } // Create audit entry (non-critical - don't fail the build if audit fails) auditEntry := &domain.BuildAuditEntry{ TaskID: taskID, ProjectID: projectID, Spec: spec, Status: domain.BuildStatusPending, StartedAt: time.Now(), } log := logging.FromContext(ctx).WithService("build") if err := s.audit.Record(ctx, auditEntry); err != nil { log.Warn("failed to record audit entry", "task_id", taskID, logging.FieldError, err.Error(), ) } log.Info("build enqueued", "task_id", taskID, logging.FieldProjectID, projectID, "template", spec.Template, "auto_push", spec.AutoPush, ) return taskID, nil } // GetBuildStatus returns the current status of a build. func (s *BuildService) GetBuildStatus(ctx context.Context, taskID string) (*domain.BuildAuditEntry, error) { return s.audit.Get(ctx, taskID) } // ListBuilds returns build history for a project. func (s *BuildService) ListBuilds(ctx context.Context, projectID string, limit int) ([]*domain.BuildAuditEntry, error) { if limit <= 0 { limit = 50 } return s.audit.List(ctx, port.BuildAuditFilter{ ProjectID: projectID, Limit: limit, }) } // CompleteBuild updates the audit entry when a build finishes. // Called by the work queue processor on task completion. func (s *BuildService) CompleteBuild(ctx context.Context, taskID string, result *domain.BuildResult) error { if err := s.audit.Update(ctx, taskID, result); err != nil { return fmt.Errorf("update audit: %w", err) } log := logging.FromContext(ctx).WithService("build") log.Info("build completed", "task_id", taskID, "success", result.Success, logging.FieldDuration, result.DurationMs, ) return nil } // StartBuildWithSDLCContext enqueues a build task with SDLC context for callback routing. // The SDLC context is included in the task spec and will be passed through to the callback. func (s *BuildService) StartBuildWithSDLCContext(ctx context.Context, projectID string, spec domain.BuildSpec, sdlcCtx map[string]any) (string, error) { if err := spec.Validate(); err != nil { return "", err } if projectID == "" { return "", fmt.Errorf("project_id is required") } // Build work task spec from build spec taskSpec := map[string]any{ "prompt": spec.Prompt, "auto_commit": spec.AutoCommit, "auto_push": spec.AutoPush, } if spec.Template != "" { taskSpec["template"] = spec.Template } if len(spec.Variables) > 0 { taskSpec["variables"] = spec.Variables } if spec.GitCloneURL != "" { taskSpec["git_clone_url"] = spec.GitCloneURL } // Add SDLC context for callback routing if sdlcCtx != nil { taskSpec["sdlc_context"] = sdlcCtx } // Create work task task := &domain.WorkTask{ ProjectID: projectID, Type: domain.WorkTaskTypeBuild, Spec: taskSpec, CallbackURL: spec.CallbackURL, MaxRetries: 3, } // Enqueue to work queue taskID, err := s.queue.Enqueue(ctx, task) if err != nil { return "", fmt.Errorf("enqueue build task: %w", err) } // Create audit entry (non-critical - don't fail the build if audit fails) auditEntry := &domain.BuildAuditEntry{ TaskID: taskID, ProjectID: projectID, Spec: spec, Status: domain.BuildStatusPending, StartedAt: time.Now(), } log := logging.FromContext(ctx).WithService("build") if err := s.audit.Record(ctx, auditEntry); err != nil { log.Warn("failed to record audit entry", "task_id", taskID, logging.FieldError, err.Error(), ) } log.Info("build enqueued with SDLC context", "task_id", taskID, logging.FieldProjectID, projectID, "sdlc_context", sdlcCtx, ) return taskID, nil }