reviewgroups

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 25 Imported by: 0

README

Review groups

A review group is a named set of open pull requests of one repository reviewed as one: either chosen by a person in the inbox ("Review together"), when the members need not be stacked or related by branch, or made by baum for a stack of pull requests (a stack group, below). Generating the group writes one combined baum.review/1 document across every member: how the changes fit together, risks that cross members (the same file or function changed twice, conflicting assumptions, merge order), and each member's own findings in sections attributed to the members whose hunks they cover.

This package owns the group definitions, the group review versions and the /api/groups routes. Generation itself runs through the job queue (internal/jobs) and generate.Publish, like a pull request review.

Rules

  • A group has a name (1 to 80 characters, the schema's stack.title bound) and 2 to 8 members (MaxMembers), all pull requests of one repository. A request naming pull requests of several repositories is refused with cross_repository; groups across repositories are a later step.
  • Members are kept in ascending number order. Renaming, adding and removing members and deleting the group are allowed at any time; a group never has fewer than two members. A stack group is the exception: it may have one open member while the rest of its stack merged (landed, below).
  • Deleting a group removes it from every list. It never deletes a saved review: the members' own reviews stay in their clone's store and the group's combined versions stay in app.sqlite, unreachable.
  • Generating needs a registered, available clone of the repository and a saved review of every member (any version). The member reviews are input to the group run and the schema records, per member, the review version the group review read (stack.members[].review_version, required). A member without a saved review is refused before any model call with members_need_reviews, naming them; the app offers to generate those reviews first.
  • A group review is outdated when its latest version covered a different set of members, or a member's head (from discovery) differs from the head the version reviewed. An unknown current head is not outdated.

Stack groups

Stacked pull requests are grouped by default. After every complete discovery poll the daemon finds the repository's stacks (Stacks) and keeps its stack groups in step (Store.SyncStacks):

  • A stack follows the hosted platform's rules, shared in stacks (stacks.Plan): pull requests by one author, each based on another's head branch (base_ref = head_ref), as a chain or a tree; forks take no part. The open pull requests are planned over the live stack groups, which keeps a group's identity when its stack changes. A stack of more than eight open pull requests gets no group.
  • A member that merged or closed stays in its stack group, in its place, in landed with its state (merged, closed or unknown): it is no longer in members, so no review, automatic job or outdated check reads it, and the group page lists these first, marked Merged, Closed or No longer open. Discovery lists only open pull requests, so the group keeps each member's parent, branches and author (groups.chain, written by every sync) and plans a member discovery no longer lists from that. The state comes from discovery's observations and is kept once known. A member with no observation yet (or a stale open one) is unknown until the next poll after its merged or closed state is observed, a brief window, and a stack group saved before chain existed has no saved branches, so a member that merges before its first sync leaves it. The group goes on while one member is open, even if only one; automatic generation needs two open members.
  • A stack without a group gets one (stack true), named after its bottom pull request's title as "Review together" names a group after the first, until its review names it: a stack group's name is baum's, so every stored version of its review renames it to the name the model wrote for the group (see Generation). The app shows a stack group, and its automatically generated review, as it shows a group a person made and generated, marked as a stack: the hosted list's stack icon and count on its row, and "Stack" instead of "Review group" on its page. A stack that gained or lost members (a parent merged, a pull request added on top) gives its group the new members; one that is gone (no open member left, or the stack split apart) dissolves its group, which leaves every list like a deleted one and keeps its versions.
  • The person's choices win. A pull request in a group the person made, or in a stack group the person deleted, takes no part in stacks. Renaming a stack group or changing its members makes it the person's (stack false), so sync no longer changes or dissolves it.
  • With automatic generation on for the repository, a stack group's review follows its members' (see internal/automatic): it is queued once every member is eligible, has a saved review of its current head and no automatic job left to submit, unless the group's latest version already read every current member at that head (a merged member alone calls for none), and once automatic generation handled at least one member's current head: a stack reviewed before automatic generation was enabled, or only by hand, is not caught up. Otherwise, and for groups a person made, the group review is generated on request.

Storage

The group tables share the private app.sqlite application database in the state directory, not a clone: a group outlives any one clone.

Table Holds
groups(id, repository, name, members, stack, chain, created, updated, deleted) definitions; members is a JSON array of numbers; stack marks a stack group; chain is a stack group's saved placements (parent, branches, author, merged flag) for every member, merged ones included; deleted is a soft-delete time, 0 while live
versions(group_id, version, metadata, members, document) immutable combined reviews keyed by group ID and version; metadata is store.Metadata; members is the reviewed stack.members copied out of the document for listing

Group versions are keyed by group ID, never by {repository, number}, so they never collide with a member's own versions in the clone's versions.sqlite. Store.Versions(id) adapts a group to the generate.Versions interface generate.Publish takes: Put appends only while the expected version is still the latest. Run leases are not recorded for groups; the job queue shows a running group job.

Generation

A job item names {clone, group} instead of {clone, pr}; its pr is 0, so no pull request logic (automatic scheduling of pull requests, notices) matches a group job. An automatic group job's head is its members' heads (Prepared.Members): it is busy or a duplicate only against jobs of the same group. Preparation reads every member's head, state and author permission from GitHub like a pull request job: a member from a fork or an author without known write access needs the isolation confirmation for the whole group. Its budgets are one run's, as for a pull request. A queued job is rechecked before running: every member head must be the one it started from.

generate.Run with Options.Group:

  1. Assembles each member with the ordinary assembler (no description images), each into its own checkout archive. Each member's checkout gets an equal share of the checkout byte and file budgets.
  2. Merges them (assemble.MergeGroup) into one bundle: the group's name and a list of the members with their descriptions as the pull request header (number and branches of the lowest member), every member's hunks in member order with duplicates (the same change in two members) listed once, the union of checks, conversation, threads, sessions and references, and FileComparisons with one comparison per member. The merged bundle is fitted to the configured caps again and digested.
  3. Unpacks each member's checkout at members/<number>/head/ and members/<number>/merge-base/ in the run directory, and writes members.json: each member's number, title, reviewed head and a compact projection of its saved review (summary, sections and risks). A section whose details are pending or failed has "Details for this section are unavailable." in place of its summary.
  4. Runs the agent loop with the public group prompt (agent/prompt GroupPrompts, version group-review-public-v4). The model names the group: its schema asks for stack with only a title (agent.Run.ModelSchema). Before validation the host fills the rest of stack (members = number, head SHA and member review version) and each section's pull_requests, the members whose hunks the section lists. The title is the model's name, on one line and cut to 80 characters, for a stack group, and the group's name for a group the person made or took over, or when the model wrote none. A previous group version's sections keep only the members still in the group.
  5. Store.Put stores the version and, in the same transaction, renames a group that is still a stack group to the document's stack.title when the name rules accept it. A person's rename made the group theirs (stack false) and is never written over.

The stored document is a stack-shaped baum.review/1: review.pull_request is the lowest member's number, as a hosted stack review holds its root member, and stack names the members. No schema change.

Viewing

The group page renders the combined review with the shared viewer (the group name as heading, member labels on sections). Patches come from GET /api/groups/{id}/patch, which picks the member whose comparison (index.file_comparisons) changes the path and whose hunk header matches the cited hunk's range. The patch is masked as a stored hunk is (locallink.Patcher, bundle.RedactFilePatch), as for a pull request's GET /api/patch. Sessions and commits are not listed for a group in v1; the viewer shows the sessions the document recorded.

HTTP

Behind the daemon's authenticated host. JSON is decoded strictly; errors are {"code","message"}.

Route Result
GET /api/groups {"available":true,"groups":[Group]}
POST /api/groups {"name","members":[{"repository","number"}]} create (201); cross_repository, invalid_group, not_picked (400)
GET /api/groups/{id} one group (404 when missing or deleted)
PATCH /api/groups/{id} {"name"?,"members"?:[number]} rename or set members
DELETE /api/groups/{id} delete (204)
GET /api/groups/{id}/versions store.Metadata newest first
GET /api/groups/{id}/document?version= the latest or the named version
GET /api/groups/{id}/patch?version=&path=&hunk= {"patch": string or null}

Group is {id, repository, name, members, stack, landed, created_at, updated_at, version_count, latest, risk_score, reviewed, outdated}: latest is the latest version's metadata (or null), reviewed the members it covered with their heads, and outdated as above. Every mutation publishes an index event.

Documentation

Overview

Package reviewgroups keeps review groups: named sets of open pull requests of one repository that a person reviews together, and the versions of their combined review. See README.md.

Index

Constants

View Source
const (
	StateMerged  = "merged"
	StateClosed  = "closed"
	StateUnknown = "unknown"
)

The states of a member that is no longer open (chainEntry.State).

View Source
const (
	MinMembers = 2
	MaxMembers = 8
	// MaxNameLength is the schema's bound on a stack title, which the
	// group's name becomes.
	MaxNameLength = 80
)

MaxMembers bounds a group's pull requests; MinMembers is the fewest a group keeps, since one pull request is reviewed on its own.

View Source
const (
	CodeInvalid         = "invalid_group"
	CodeCrossRepository = "cross_repository"
	CodeNotPicked       = "not_picked"
)

The codes of InvalidError.

Variables

View Source
var ErrConflict = errors.New("the group review changed during the operation; generate again")

ErrConflict is a version that another generation published first.

View Source
var ErrNotFound = errors.New("review group not found")

ErrNotFound is a group that does not exist or was deleted.

Functions

func MembersFile

func MembersFile(members []MemberReview) ([]byte, error)

MembersFile is members.json, the members' saved reviews in brief for the group run: each member's title, the head the run reviews, and its review's summary, current sections and active risks. Stale sections and mitigated risks are left out, and so is what does not fit a member's share.

func Outdated

func Outdated(members []int, reviewed []ReviewedMember, heads map[int]string) bool

Outdated reports whether a group review that covered reviewed no longer matches the group: its members changed, or a member's current head (from discovery; an unknown head is not counted) differs from the head reviewed. A group without a review is not outdated.

Types

type API

type API struct {
	Store    *Store
	Registry Registry
	// Index is the review index, for the members' current heads; nil leaves
	// every head unknown, so no group reads as outdated.
	Index func(context.Context) (reviewindex.Snapshot, error)
	// Changed is called after every mutation.
	Changed func()
	// Patch reads one file's patch; nil uses a locallink.Patcher.
	Patch func(context.Context, locallink.PatchRequest) (locallink.PatchResult, error)
	// File reads one file at a commit; nil uses locallink.ReadFile.
	File func(context.Context, locallink.FileRequest) (locallink.FileResult, error)
	// contains filtered or unexported fields
}

API serves /api/groups. It has no authentication; the daemon mounts it behind its authenticated host.

func (*API) Close

func (h *API) Close()

Close closes the clones the API's own Patcher keeps open. A Patch the caller supplied is the caller's to close.

func (*API) Handler

func (h *API) Handler() http.Handler

Handler routes /api/groups.

type Group

type Group struct {
	ID string `json:"id"`
	// Repository is the registry's identity of the members' repository:
	// lowercase owner/name, or host/owner/name off github.com.
	Repository string `json:"repository"`
	Name       string `json:"name"`
	// Members are the pull request numbers, ascending.
	Members []int `json:"members"`
	// Stack marks a group baum made for a stack of pull requests and keeps
	// in step with it (SyncStacks); a person's rename or member change makes
	// it theirs.
	Stack bool `json:"stack"`
	// Landed are the members of a stack group that are no longer open,
	// ascending, with what became of them. They stay in the stack, in
	// place, but are not Members: a review never reads them. Empty for a
	// group a person made.
	Landed    []LandedMember `json:"landed"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	// contains filtered or unexported fields
}

Group is one review group's definition.

type InvalidError

type InvalidError struct {
	Code    string
	Message string
}

InvalidError is a group definition the rules refuse; Code is the machine-readable reason the API answers with.

func (*InvalidError) Error

func (e *InvalidError) Error() string

type LandedMember

type LandedMember struct {
	Number int    `json:"number"`
	State  string `json:"state"`
}

LandedMember is a stack group member that is no longer open. State is StateMerged, StateClosed or StateUnknown (not open any more, but discovery has not yet seen whether it merged).

type MemberReview

type MemberReview struct {
	Number int
	// Head is the member's head the group run reviews.
	Head string
	// Version and Document are the member's latest saved review.
	Version  int
	Document []byte
}

MemberReview is one member's saved review, as a group run reads it.

type Publisher

type Publisher struct {
	Store *Store
	Group string
}

Publisher is the group as generate.Publish's version store: it ignores the pull request key and records no run leases (the job queue shows a running group job).

func (Publisher) BeginRun

BeginRun implements generate.Versions.

func (Publisher) FinishRun

func (Publisher) FinishRun(context.Context, int64, string) error

FinishRun implements generate.Versions.

func (Publisher) HeartbeatRun

func (Publisher) HeartbeatRun(context.Context, int64, time.Time) error

HeartbeatRun implements generate.Versions.

func (Publisher) Latest

func (p Publisher) Latest(ctx context.Context, _ store.Key) (store.Version, error)

Latest implements generate.Versions.

func (Publisher) Put

func (p Publisher) Put(ctx context.Context, _ store.Key, previous, next store.Version) error

Put implements generate.Versions.

type Registry

type Registry interface {
	Repositories(context.Context) ([]repos.Repository, error)
}

Registry is the part of *repos.Registry the handler reads: the picked repositories and their clones.

type ReviewedMember

type ReviewedMember struct {
	Number        int    `json:"number"`
	HeadSHA       string `json:"head_sha"`
	ReviewVersion int    `json:"review_version"`
}

ReviewedMember is a member as a group review version covered it: the document's stack member.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store keeps group definitions and versions in the application database.

func Open

func Open(path string) (*Store, error)

Open creates or opens a private store at path.

func (*Store) Close

func (s *Store) Close() error

Close closes the database.

func (*Store) Create

func (s *Store) Create(ctx context.Context, repository, name string, members []int) (Group, error)

Create stores a new group of members of repository.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, id string) error

Delete removes the group from every list. Its versions stay in the store, and the members' own reviews are never touched. A deleted stack group keeps its flag, so SyncStacks makes no new group for its pull requests.

func (*Store) Get

func (s *Store) Get(ctx context.Context, id string) (Group, error)

Get returns one group, ErrNotFound when it does not exist or was deleted.

func (*Store) Latest

func (s *Store) Latest(ctx context.Context, id string) (store.Version, error)

Latest returns the group's latest version, store.ErrNotFound when it has none.

func (*Store) List

func (s *Store) List(ctx context.Context) ([]Group, error)

List returns the groups that are not deleted, newest first.

func (*Store) Put

func (s *Store) Put(ctx context.Context, id string, previous int, v store.Version) error

Put appends v only while previous is still the group's latest version (0 for none). The document must be the group's: a review of its repository with a stack, at version previous+1. A stack group, whose name is baum's, takes the name the review gave it (the document's stack title); a group the person made or took over keeps theirs.

func (*Store) Summary

func (s *Store) Summary(ctx context.Context, id string) (Summary, error)

Summary returns what the list shows of the group's versions.

func (*Store) SyncStacks

func (s *Store) SyncStacks(ctx context.Context, repository string, prs []discovery.PullRequest, observations []discovery.Observation) (bool, error)

SyncStacks keeps repository's stack groups in step with the stacks among its pull requests (package stacks, the hosted detection) and reports whether anything changed. prs are the open pull requests and observations what discovery knows of others (a merged or closed state):

  • a stack of 2 to MaxMembers open pull requests without a group gets a new stack group;
  • a stack group whose stack gained or lost members gets the stack's members;
  • a member that merged or closed stays in the group in its place, as Landed with its state, and is no longer a Member. A member discovery no longer lists and has no observation of is kept as StateUnknown until its state is known;
  • a stack group whose stack is gone (no open member left, unstacked, or grown past MaxMembers open pull requests) is dissolved: removed from every list, its versions kept.

The person's choices win: a pull request in a group the person made or took over, or in a stack group the person deleted, takes no part.

func (*Store) Update

func (s *Store) Update(ctx context.Context, id string, name *string, members []int) (Group, error)

Update renames the group when name is not nil and replaces its members when members is not nil. Either makes a stack group the person's own.

func (*Store) Version

func (s *Store) Version(ctx context.Context, id string, version int) (store.Version, error)

Version returns one version, store.ErrNotFound when it does not exist.

func (*Store) Versions

func (s *Store) Versions(ctx context.Context, id string) ([]store.Metadata, error)

Versions returns the metadata of every version, newest first.

type Summary

type Summary struct {
	VersionCount int             `json:"version_count"`
	Latest       *store.Metadata `json:"latest"`
	// RiskScore is the latest version's summary.risk_score, or null.
	RiskScore json.RawMessage  `json:"risk_score"`
	Reviewed  []ReviewedMember `json:"reviewed"`
}

Summary is what the group list shows of a group's versions.

type View

type View struct {
	Group
	Summary
	Outdated bool `json:"outdated"`
}

View is a group as the API answers it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL