"""A work item's place in the loop (L1), kept in its row in the state store. The row's ``position`` is the item's state; what the loop needs besides (the thread its conversation lives in, the revision of what is put up for agreement, what it is waiting for) is in ``data["loop"]``. """ from dataclasses import asdict, dataclass from typing import Any from nisse_dev.state import StateStore, WorkItem READY = "ready" RUNNING = "running" # a driver run is under way; on boot it means interrupted WAITING = "waiting" BLOCKED = "blocked" # waits for its recorded dependencies to close (L1) CLOSED = "closed" # What a waiting item waits for. GO_AHEAD = "go-ahead" # a go on the restatement up (L3, D1) ANSWER = "answer" # an answer to a question MERGE = "merge" # the PR's review and merge (D4, D5) CHAT = "conversation" # a conversation waits for the operator to say more (L5) @dataclass(frozen=True) class Loop: thread: str = "" # the root of the item's Matrix thread (L5) issue: int = 0 # its record issue (D1) wait: str = "" # GO_AHEAD / ANSWER / MERGE, when waiting rev: int = 0 # the latest proposal put up (L3) started: bool = False # scope agreed: a go-ahead was given (D1) note: str = "" # what a waiting item said it is waiting on prepared: bool = False # its fresh workspace was made (D3) milestone: int = 0 # the current milestone (D4) branch: str = "" # the current milestone's branch, while one is open pr: int = 0 # the current milestone's PR, once published pr_url: str = "" comments: int = 0 # the last PR conversation comment taken (L8) notes: tuple[str, ...] = () # review notes taken (#69) fixes: int = 0 # review fixes pushed, for follow-up branch names blocked_by: tuple[str, ...] = () # open dependencies, "owner/repo#N" (L1) first: int = 0 # the operator's "do #N next": higher runs sooner labels: tuple[str, ...] = () # the issue's labels at intake: they steer (D1) grace_until: float = 0.0 # a `bug` restatement proceeds then, unanswered talked: bool = False # the operator has spoken in the item's thread # A conversation (`general`, L1): when it last heard or said anything, # the conversation it continues, the work it turned into, and the work # proposed for a go-ahead. idle_since: float = 0.0 follows: str = "" became: tuple[str, ...] = () proposal_project: str = "" proposal_title: str = "" # Where the operator last spoke (D5: answer where asked): "" for the # Matrix thread, or the PR it came from, with the inline location and a # little of what they said, for quoting. asked_on_pr: int = 0 asked_where: str = "" asked_text: str = "" def to_data(self) -> dict[str, Any]: return asdict(self) def loop_of(item: WorkItem) -> Loop: data = dict(item.data.get("loop", {})) data["notes"] = tuple(data.get("notes", ())) data["blocked_by"] = tuple(data.get("blocked_by", ())) data["labels"] = tuple(data.get("labels", ())) data["became"] = tuple(data.get("became", ())) return Loop(**data) def save( store: StateStore, item_id: str, state: str, loop: Loop, *, cause: str ) -> None: item = store.work_item(item_id) assert item is not None store.update_work_item( item_id, position=state, data={**item.data, "loop": loop.to_data()}, cause=cause, ) def item_for_thread( store: StateStore, thread: str, status: str | None = "open" ) -> WorkItem | None: """The item whose conversation is ``thread``; the latest, when a closed conversation's thread was picked up again.""" found = [i for i in store.work_items(status) if loop_of(i).thread == thread] return found[-1] if found else None def item_for_issue( store: StateStore, issue: int, project: str | None = None ) -> WorkItem | None: """Any item, open or closed, recorded by this issue (in ``project``, when given): intake never takes an issue twice (D1).""" for item in store.work_items(): if loop_of(item).issue == issue and project in (None, item.scope): return item return None