package workitem import ( "context" "encoding/json" "errors" "strings" "github.com/nomadcode/nomadcode-core/internal/workflow" ) var ErrInvalidRef = errors.New("invalid workitem ref") type ProviderID string type Capability string const ( CapabilityLookup Capability = "lookup" CapabilityComment Capability = "comment" CapabilityStatusProjection Capability = "status_projection" CapabilityLabelProjection Capability = "label_projection" CapabilityCreate Capability = "create" CapabilityBodyProjection Capability = "body_projection" ) type Capabilities map[Capability]bool func (c Capabilities) Supports(capability Capability) bool { return c[capability] } type Provider interface { ProviderID() ProviderID Capabilities() Capabilities } type Reader interface { FetchWorkItem(ctx context.Context, ref Ref) (WorkItem, error) } type Commenter interface { AppendComment(ctx context.Context, input CommentInput) error } type StatusProjector interface { ProjectStatus(ctx context.Context, input StatusProjection) error } type Creator interface { CreateWorkItem(ctx context.Context, input CreateInput) (CreateResult, error) } // BodyProjector updates an existing work item's title and/or body. It is an // additive projection capability separate from status projection so a body // rewrite and a status move stay independent steps. type BodyProjector interface { ProjectBody(ctx context.Context, input BodyProjection) error } type LabelProjector interface { ProjectLabels(ctx context.Context, input LabelProjection) error } type Ref struct { Provider ProviderID `json:"provider"` Tenant string `json:"tenant"` Project string `json:"project"` ID string `json:"id"` URL string `json:"url,omitempty"` Metadata json.RawMessage `json:"metadata,omitempty"` } type WorkItem struct { Ref Ref `json:"ref"` Title string `json:"title"` DescriptionText string `json:"description_text,omitempty"` DescriptionHTML string `json:"description_html,omitempty"` State string `json:"state,omitempty"` AssigneeIDs []string `json:"assignee_ids,omitempty"` Labels []string `json:"labels,omitempty"` Metadata json.RawMessage `json:"metadata,omitempty"` } func (w WorkItem) HasAssignee(id string) bool { target := strings.TrimSpace(id) if target == "" { return false } for _, a := range w.AssigneeIDs { if strings.TrimSpace(a) == target { return true } } return false } type CommentFormat string const ( CommentFormatText CommentFormat = "text" CommentFormatMarkdown CommentFormat = "markdown" CommentFormatHTML CommentFormat = "html" ) type CommentInput struct { Ref Ref `json:"ref"` Body string `json:"body"` Format CommentFormat `json:"format"` ExternalSource string `json:"external_source,omitempty"` ExternalID string `json:"external_id,omitempty"` Visibility string `json:"visibility,omitempty"` } type StatusProjection struct { Ref Ref `json:"ref"` CanonicalState string `json:"canonical_state"` ProviderState string `json:"provider_state"` } type BodyProjection struct { Ref Ref `json:"ref"` Title string `json:"title,omitempty"` DescriptionHTML string `json:"description_html,omitempty"` } type CreateInput struct { Provider ProviderID `json:"provider"` Tenant string `json:"tenant"` Project string `json:"project"` Title string `json:"title"` DescriptionText string `json:"description_text,omitempty"` DescriptionHTML string `json:"description_html,omitempty"` } type CreateResult struct { Ref Ref `json:"ref"` } type LabelTarget struct { Canonical string `json:"canonical"` Provider string `json:"provider"` } type LabelProjection struct { Ref Ref `json:"ref"` Add []LabelTarget `json:"add,omitempty"` Remove []LabelTarget `json:"remove,omitempty"` } type Mapping struct { States map[string]string `json:"states,omitempty"` Labels map[string]string `json:"labels,omitempty"` } func (m Mapping) StateTarget(canonical string) (string, bool) { target := strings.TrimSpace(m.States[strings.TrimSpace(canonical)]) return target, target != "" } func (m Mapping) LabelTarget(canonical string) (string, bool) { target := strings.TrimSpace(m.Labels[strings.TrimSpace(canonical)]) return target, target != "" } type TaskCreateInput struct { Ref Ref StateID string Comment string } func firstNonEmpty(ss ...string) string { for _, s := range ss { if trimmed := strings.TrimSpace(s); trimmed != "" { return trimmed } } return "" } func BuildCreateTaskInput(input TaskCreateInput, item WorkItem) (workflow.CreateTaskInput, error) { ref, err := NormalizeRef(input.Ref) if err != nil { return workflow.CreateTaskInput{}, err } title := strings.TrimSpace(item.Title) if title == "" { title = ref.ID } message := firstNonEmpty(input.Comment, item.DescriptionText, item.DescriptionHTML, title) metadata := map[string]string{ "provider": string(ref.Provider), "tenant": ref.Tenant, "project": ref.Project, "id": ref.ID, "state_id": strings.TrimSpace(input.StateID), "external_url": ref.URL, } payloadBytes, err := json.Marshal(map[string]any{ "message": message, "work_item": metadata, }) if err != nil { return workflow.CreateTaskInput{}, err } metadataBytes, err := json.Marshal(metadata) if err != nil { return workflow.CreateTaskInput{}, err } return workflow.CreateTaskInput{ Title: title, Source: string(ref.Provider), Payload: payloadBytes, External: &workflow.ExternalRefInput{ Provider: string(ref.Provider), ID: ref.ID, URL: ref.URL, Metadata: metadataBytes, }, }, nil } func NormalizeRef(ref Ref) (Ref, error) { provider := ProviderID(strings.TrimSpace(string(ref.Provider))) id := strings.TrimSpace(ref.ID) if provider == "" || id == "" { return Ref{}, ErrInvalidRef } meta := ref.Metadata if len(meta) == 0 { meta = json.RawMessage("{}") } else if !json.Valid(meta) { return Ref{}, ErrInvalidRef } return Ref{ Provider: provider, Tenant: strings.TrimSpace(ref.Tenant), Project: strings.TrimSpace(ref.Project), ID: id, URL: strings.TrimSpace(ref.URL), Metadata: meta, }, nil }