package domain import ( "errors" "fmt" "net/url" "strconv" ) // Verify-related errors. var ( ErrVerifyURLRequired = errors.New("url is required for verify spec") ) // VerifySpec defines what a verify task should accomplish. type VerifySpec struct { // URL is the page to capture (required). URL string `json:"url"` // Viewports is a list of viewport sizes to capture. // Default: ["1920x1080", "768x1024", "375x667"] Viewports []string `json:"viewports,omitempty"` // WaitFor is a CSS selector to wait for before capturing. // Default: "body" WaitFor string `json:"wait_for,omitempty"` // WaitTimeout is the maximum time to wait for the selector in milliseconds. // Default: 10000 WaitTimeout int `json:"wait_timeout,omitempty"` // FullPage captures the entire scrollable page if true. FullPage bool `json:"full_page,omitempty"` // Video records a video of the page load if true. Video bool `json:"video,omitempty"` // Evaluate enables AI evaluation of the captures (Week 3). Evaluate bool `json:"evaluate,omitempty"` // Prompt provides context for AI evaluation (Week 3). Prompt string `json:"prompt,omitempty"` // CallbackURL is the webhook URL for completion notification. CallbackURL string `json:"callback_url,omitempty"` } // DefaultViewports returns the default viewport sizes for captures. func DefaultViewports() []string { return []string{"1920x1080", "768x1024", "375x667"} } // Validate checks that the VerifySpec has all required fields. func (s *VerifySpec) Validate() error { if s.URL == "" { return ErrVerifyURLRequired } // Validate URL format u, err := url.Parse(s.URL) if err != nil { return fmt.Errorf("invalid url: %w", err) } if u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("url scheme must be http or https, got %q", u.Scheme) } // Validate callback URL if provided if s.CallbackURL != "" { cu, err := url.Parse(s.CallbackURL) if err != nil { return fmt.Errorf("invalid callback_url: %w", err) } if cu.Scheme != "http" && cu.Scheme != "https" { return fmt.Errorf("callback_url scheme must be http or https, got %q", cu.Scheme) } } return nil } // WithDefaults returns a copy of the spec with default values applied. func (s *VerifySpec) WithDefaults() *VerifySpec { spec := *s if len(spec.Viewports) == 0 { spec.Viewports = DefaultViewports() } if spec.WaitFor == "" { spec.WaitFor = "body" } if spec.WaitTimeout == 0 { spec.WaitTimeout = 10000 } return &spec } // VerifyResult captures the outcome of a verify execution. type VerifyResult struct { // Success indicates whether the capture completed successfully. Success bool `json:"success"` // Screenshots maps viewport size to screenshot file path. // Example: {"1920x1080": "/captures/task-id/1920_1080.png"} Screenshots map[string]string `json:"screenshots,omitempty"` // Video is the path to the recorded video if requested. Video string `json:"video,omitempty"` // Evaluation contains the AI evaluation result (Week 3). Evaluation string `json:"evaluation,omitempty"` // Score is the AI-assigned score 0-100 (Week 3). Score int `json:"score,omitempty"` // Passed indicates whether the AI evaluation passed (Week 3). Passed bool `json:"passed,omitempty"` // DurationMs is how long the capture took in milliseconds. DurationMs int64 `json:"duration_ms"` // Error contains the error message if capture failed. Error string `json:"error,omitempty"` } // ToWorkResult converts a VerifyResult to a WorkResult for queue compatibility. // Screenshots are promoted to artifacts with viewport as key prefix. func (r *VerifyResult) ToWorkResult() *WorkResult { if r == nil { return &WorkResult{} } wr := &WorkResult{} // Use error as output if failed, otherwise empty (captures are in artifacts) if !r.Success && r.Error != "" { wr.Output = r.Error } // Promote screenshots and metadata to artifacts if len(r.Screenshots) > 0 || r.Video != "" || r.DurationMs > 0 { wr.Artifacts = make(map[string]string) // Add screenshots for viewport, path := range r.Screenshots { wr.Artifacts["screenshot_"+viewport] = path } // Add video if present if r.Video != "" { wr.Artifacts["video"] = r.Video } // Add duration if r.DurationMs > 0 { wr.Artifacts["duration_ms"] = strconv.FormatInt(r.DurationMs, 10) } // Add evaluation results (Week 3) if r.Evaluation != "" { wr.Artifacts["evaluation"] = r.Evaluation } if r.Score > 0 { wr.Artifacts["score"] = strconv.Itoa(r.Score) } if r.Passed { wr.Artifacts["passed"] = "true" } } return wr } // ToBuildResult converts a VerifyResult to a BuildResult for executor compatibility. // This allows the verify executor to return results through the work queue. func (r *VerifyResult) ToBuildResult() *BuildResult { if r == nil { return &BuildResult{} } br := &BuildResult{ Success: r.Success, DurationMs: r.DurationMs, Error: r.Error, } // Promote screenshots and video to artifacts if len(r.Screenshots) > 0 || r.Video != "" { br.Artifacts = make(map[string]string) for viewport, path := range r.Screenshots { br.Artifacts["screenshot_"+viewport] = path } if r.Video != "" { br.Artifacts["video"] = r.Video } } return br }