Files
nisse/nisse-review/src/nisse_review/state.py
T
dongarandClaude Opus 5.5 37bbac7e72 nisse-review: later reviews, replies, rounds, stop and review (M7.5)
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
2026-10-17 07:06:06 +00:00

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)}