package operations import ( "encoding/json" "errors" "fmt" "orchestra/internal/authz" "orchestra/internal/domain" "orchestra/internal/registry" "orchestra/internal/review" "orchestra/internal/store" ) // ErrReviewNotEligible reports that the entry conditions for review are not // met. It names which one, because "not eligible" alone sends an operator // reading code. var ErrReviewNotEligible = errors.New("not eligible for review") // EnterReview checks the entry conditions and moves the task to the review // phase, which is what makes the next session a reviewing session. // // The conditions exist so a reviewer is never handed an unfinished or // unanchored change: reviewing a tree that nobody can reproduce produces // findings nobody can act on. func EnterReview(s *store.Store, project registry.Project, taskID string, ev review.Evidence) (domain.Event, error) { t, ok := s.Task(taskID) if !ok { return domain.Event{}, domain.ErrNotFound } if current(t) != domain.WorkPhaseImplement { return domain.Event{}, fmt.Errorf("%w: work phase is %s, not implement", ErrReviewNotEligible, current(t)) } if t.State == domain.StateBlocked || t.State == domain.StateNeedsAttention { return domain.Event{}, fmt.Errorf("%w: task is %s (%s)", ErrReviewNotEligible, t.State, t.BlockReason) } if t.DecisionRequest != nil { return domain.Event{}, fmt.Errorf("%w: an unresolved human decision is outstanding", ErrReviewNotEligible) } if len(ev.ResultSHA) != 40 || len(ev.BaseSHA) != 40 { return domain.Event{}, fmt.Errorf("%w: base and result commits must both be anchored", ErrReviewNotEligible) } if ev.Diff == "" { return domain.Event{}, fmt.Errorf("%w: there is no diff to review", ErrReviewNotEligible) } if ev.GateCommand != "" && ev.GateExit != 0 { return domain.Event{}, fmt.Errorf("%w: quality gate %q exited %d", ErrReviewNotEligible, ev.GateCommand, ev.GateExit) } if project.QualityGate != "" && ev.GateCommand == "" { return domain.Event{}, fmt.Errorf("%w: project requires the quality gate to have run", ErrReviewNotEligible) } return advanceWorkPhase(s, project, taskID, nil, map[string]any{"result_sha": ev.ResultSHA}) } // RecordReview seals a review against the exact commit it examined, then acts // on it. Blocking findings return the task to implementation with the findings // in hand. Minor findings are recorded and left alone. // // The reviewing session supplies findings and nothing else. It does not decide // the phase, and it never edits code. func RecordReview(s *store.Store, project registry.Project, taskID string, result review.Result) (domain.Event, error) { if err := result.Validate(); err != nil { return domain.Event{}, fmt.Errorf("%w: %s", domain.ErrInvalid, err) } t, ok := s.Task(taskID) if !ok { return domain.Event{}, domain.ErrNotFound } if current(t) != domain.WorkPhaseReview { return domain.Event{}, fmt.Errorf("%w: work phase is %s, not review", domain.ErrInvalid, current(t)) } // A review of a different commit is not a review of this work. Catching it // here beats discovering it at completion, when the reviewing session is // already gone. if t.ReviewTargetSHA != "" && result.ResultSHA != t.ReviewTargetSHA { return domain.Event{}, fmt.Errorf("%w: review is for %s but this phase was entered against %s", domain.ErrInvalid, result.ResultSHA, t.ReviewTargetSHA) } sealed, err := review.Encode(result) if err != nil { return domain.Event{}, err } ref, err := s.PutArtifact(sealed) if err != nil { return domain.Event{}, err } blocking := len(result.Blocking()) b, err := json.Marshal(map[string]any{ "artifact_ref": ref, "result_sha": result.ResultSHA, "blocking": blocking, }) if err != nil { return domain.Event{}, err } e := domain.Event{ID: domain.NewID(), Type: domain.EventReviewRecorded, TaskID: taskID, Version: t.Version + 1, Payload: b, Surface: string(authz.System)} if err := s.Append(e); err != nil { return domain.Event{}, err } if blocking == 0 { return e, nil } // Back to implementation, with the findings as the reason. if _, err := AdvanceWorkPhase(s, project, taskID, nil); err != nil { return e, err } return e, nil } // TaskReview loads the sealed findings for a task, for the implementation // context that has to act on them. func TaskReview(s *store.Store, t domain.Task) (*review.Result, error) { if t.Review == nil { return nil, nil } b, err := s.Artifact(t.Review.ArtifactRef) if err != nil { return nil, err } r, err := review.Decode(b) if err != nil { return nil, err } return &r, nil }