rdev/internal/service/sdlc_generate.go
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
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>
2026-02-04 22:56:04 -07:00

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
}