Back on a PR it reviewed: a new head gets a later review of the commits since (each one's diff, through the API) and of its earlier points; what was said since gets replies, answered where it was said or skipped with a reason. It signs off once and stays quiet while still happy. Past tuning.review.max_rounds it sums up for the operator and goes quiet, until a push or someone other than the PR's author joins in. An operator's `stop` quiets it; `review` asks for a fresh review. Reviews carry numbered markers and replies their point's key, so recovery posts nothing twice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
"""nisse-review's state store (architecture L8): the shared one, with its own
|
|
name and roles. A work item is one PR (R1); what the reviewer keeps about it
|
|
is in its row's data, as ``PR``."""
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Any
|
|
|
|
from nisse_core.state import store
|
|
from nisse_core.state.store import WorkItem
|
|
|
|
# A review item's positions.
|
|
READY = "ready" # a run is due: a review, a later review, or replies
|
|
RUNNING = "running" # a run is under way; on boot, interrupted
|
|
REVIEWED = "reviewed" # nothing due: watching for pushes and comments
|
|
WAITING = "waiting" # not ready: a draft or work in progress
|
|
FAILED = "failed" # runs kept failing; said so on the PR
|
|
CAPPED = "capped" # the round cap: summed up for the operator, quiet (R2)
|
|
STOPPED = "stopped" # an operator said `stop` on the PR; `review` resumes (R5)
|
|
CLOSED = "closed"
|
|
|
|
POST_REVIEW = "post_review" # the intent around posting one (L8)
|
|
|
|
# What a run is, from the item (``Scheduler._kind``).
|
|
FIRST = "first" # the whole PR: its first review, or one asked for
|
|
LATER = "later" # what changed since its last review, and its earlier points
|
|
REPLIES = "replies" # answering what was said since, at the same head
|
|
SUMMARY = "summary" # the round cap: what is still open, for the operator
|
|
|
|
|
|
class StateStore(store.StateStore):
|
|
AGENT = "nisse-review"
|
|
# The reviewer drives as "driver" (the shared views and tools expect
|
|
# it); "memory" is its pass at a close.
|
|
ACTORS = store.ACTORS | {"memory"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PR:
|
|
repo: str # owner/name
|
|
number: int
|
|
head: str = "" # the commit last seen at the PR's head
|
|
reviewed: str = "" # the commit its last review was posted on
|
|
attempts: int = 0 # failed runs at this head
|
|
seen_at: float = 0.0 # when this head was first seen (the CI wait)
|
|
# M7.5: the discussion.
|
|
seen: tuple[str, ...] = () # keys of everything said on it, taken in
|
|
fresh: tuple[str, ...] = () # keys said since its last run, to answer
|
|
rounds: int = 0 # reviews and reply sets since the last reset (R2)
|
|
posts: int = 0 # reviews it posted on the PR: their markers' numbers
|
|
happy: bool = False # its last review signed off: no repeats while so
|
|
full: bool = False # an operator asked for a fresh review (`review`)
|
|
run: str = "" # the kind of run under way, for recovery
|
|
|
|
|
|
def item_id(repo: str, number: int) -> str:
|
|
return f"{repo}#{number}"
|
|
|
|
|
|
def pr_of(item: WorkItem) -> PR:
|
|
raw = dict(item.data["pr"])
|
|
for key in ("seen", "fresh"):
|
|
raw[key] = tuple(raw.get(key) or ())
|
|
if "posts" not in raw and raw.get("reviewed"):
|
|
raw["posts"] = 1 # reviewed before M7.5, which numbered them
|
|
return PR(**raw)
|
|
|
|
|
|
def data(pr: PR) -> dict[str, Any]:
|
|
return {"pr": asdict(pr)}
|