package human import ( "context" "strings" "time" "orchestra/internal/domain" ) // ReviewObservation is one review the forge recorded on a pull request. It is // an observation, not a verdict Orchestra trusts: the trust boundary is the // actor, applied by the reconciler. type ReviewObservation struct { Actor string State string // approved | changes_requested | commented At time.Time Body string } // PullRequestState is everything Orchestra needs to know about a submitted // pull request. HeadSHA is the commit the forge believes the pull request // carries, which is how a merge is tied back to a specific submission. type PullRequestState struct { ID string HeadSHA string State string // open | merged | closed MergeSHA string MergedAt time.Time Reviews []ReviewObservation Comments []Input } // PullRequestSource reads the state of one submitted pull request. Polling is // enough: a webhook would add an inbound trust boundary for no new capability. type PullRequestSource interface { PullRequest(ctx context.Context, task domain.Task) (PullRequestState, error) } // Trust decides whose words can move a task. Without it, a bot comment or // Orchestra's own reflection could reopen a finished implementation. type Trust struct { // Accepted, when non-empty, is the allow-list of actor identities. Empty // means anyone not explicitly ignored, which is only safe on a private // forge with no bots. Accepted []string // Ignored always loses, even when it appears in Accepted. Ignored []string } // Allows reports whether this actor's words may move a task. func (t Trust) Allows(actor string) bool { actor = strings.TrimSpace(strings.ToLower(actor)) if actor == "" { return false } for _, ignored := range t.Ignored { if strings.EqualFold(strings.TrimSpace(ignored), actor) { return false } } if len(t.Accepted) == 0 { return true } for _, accepted := range t.Accepted { if strings.EqualFold(strings.TrimSpace(accepted), actor) { return true } } return false } // FeedbackAfter returns the trusted human input on a pull request that arrived // strictly after the submission. Anything at or before it was already visible // when the submission was made, so it cannot be a response to it. func (p PullRequestState) FeedbackAfter(provider string, submittedAt time.Time, trust Trust) []Input { var out []Input for _, c := range p.Comments { if !c.At.After(submittedAt) || !trust.Allows(c.Author) { continue } if strings.TrimSpace(c.Body) == "" { continue } if c.Provider == "" { c.Provider = provider } out = append(out, c) } for _, r := range p.Reviews { if !r.At.After(submittedAt) || !trust.Allows(r.Actor) { continue } if strings.TrimSpace(r.Body) == "" && r.State != "changes_requested" { continue } body := strings.TrimSpace(r.Body) if body == "" { body = "changes requested with no comment" } out = append(out, Input{ Provider: provider, ExternalID: "review:" + r.Actor + ":" + r.At.UTC().Format(time.RFC3339), Author: r.Actor, At: r.At, Body: body, }) } return out }