// Package changerequest models the change_requests entity and the server-side // merge gate. A change request links a project/workspace/(requirement|issue) // and a source->target branch to an external (Gitea) pull request, carrying a // review verdict + CI state. The crux is Service.Merge, which hard-blocks the // host-side merge unless review_verdict=='approved' (and optionally // ci_state=='success'): an agent can open and merge PRs via MCP, but cannot // self-approve — approval is HTTP/web-only by policy. // // Layout mirrors internal/workspace (single package, service + repository + // handler + sqlc) to keep the aggregate cohesive. package changerequest import ( "context" "time" "github.com/google/uuid" ) // State mirrors the change_requests.state CHECK constraint. type State string // State enum values. const ( StateOpen State = "open" StateMerged State = "merged" StateClosed State = "closed" ) // Verdict mirrors the change_requests.review_verdict CHECK constraint. type Verdict string // Verdict enum values. const ( VerdictPending Verdict = "pending" VerdictApproved Verdict = "approved" VerdictChangesRequested Verdict = "changes_requested" VerdictRejected Verdict = "rejected" ) // IsValid reports whether v is in the enum set. func (v Verdict) IsValid() bool { switch v { case VerdictPending, VerdictApproved, VerdictChangesRequested, VerdictRejected: return true } return false } // CIState mirrors the change_requests.ci_state CHECK constraint. type CIState string // CIState enum values. const ( CIUnknown CIState = "unknown" CIPending CIState = "pending" CISuccess CIState = "success" CIFailure CIState = "failure" ) // ChangeRequest is the change_requests row projection. type ChangeRequest struct { ID uuid.UUID ProjectID uuid.UUID WorkspaceID uuid.UUID RequirementID *uuid.UUID IssueID *uuid.UUID Number int Title string Description string SourceBranch string TargetBranch string State State ReviewVerdict Verdict CIState CIState Provider string ExternalID *int64 ExternalURL string MergeCommitSHA string ReviewedBy *uuid.UUID ReviewedAt *time.Time CreatedBy uuid.UUID MergedAt *time.Time CreatedAt time.Time UpdatedAt time.Time } // Caller mirrors workspace.Caller: the user context driving auth + audit. type Caller struct { UserID uuid.UUID IsAdmin bool SessionID *uuid.UUID } // CreateInput carries the fields to open a change request (and host PR). type CreateInput struct { SourceBranch string TargetBranch string // empty => workspace default branch Title string Description string RequirementID *uuid.UUID IssueID *uuid.UUID } // Service is the change-request application service. type Service interface { Create(ctx context.Context, c Caller, wsID uuid.UUID, in CreateInput) (*ChangeRequest, error) Get(ctx context.Context, c Caller, id uuid.UUID) (*ChangeRequest, error) GetByNumber(ctx context.Context, c Caller, projectSlug string, number int) (*ChangeRequest, error) ListByWorkspace(ctx context.Context, c Caller, wsID uuid.UUID) ([]*ChangeRequest, error) // Review records a human verdict (owner/admin only). It is intentionally NOT // exposed as an MCP tool so agents cannot self-approve their own PRs. Review(ctx context.Context, c Caller, id uuid.UUID, verdict Verdict, note string) (*ChangeRequest, error) // Merge is the gate: requires review_verdict=='approved' (+ ci success when // RequireCIPass), then merges the host PR and persists merged state. Merge(ctx context.Context, c Caller, id uuid.UUID, method string) (*ChangeRequest, error) // SyncStatus refreshes ci_state/state from the host (provider.GetPR). SyncStatus(ctx context.Context, c Caller, id uuid.UUID) (*ChangeRequest, error) }