alissa-tools-github-reviewloop 0.5.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {alissa_tools_github_reviewloop-0.5.0/src/main/alissa_tools_github_reviewloop.egg-info → alissa_tools_github_reviewloop-0.7.0}/PKG-INFO +1 -1
  2. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/alissa.py +42 -0
  3. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/loop.py +48 -10
  4. alissa_tools_github_reviewloop-0.7.0/src/main/alissa/tools/github/reviewloop/version +1 -0
  5. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0/src/main/alissa_tools_github_reviewloop.egg-info}/PKG-INFO +1 -1
  6. alissa_tools_github_reviewloop-0.5.0/src/main/alissa/tools/github/reviewloop/version +0 -1
  7. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/MANIFEST.in +0 -0
  8. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/README.md +0 -0
  9. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/requirements.txt +0 -0
  10. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/setup.cfg +0 -0
  11. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/setup.py +0 -0
  12. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/__init__.py +0 -0
  13. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/__main__.py +0 -0
  14. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/config.py +0 -0
  15. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/ghclient.py +0 -0
  16. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/proc.py +0 -0
  17. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/prreview.py +0 -0
  18. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/state.py +0 -0
  19. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa/tools/github/reviewloop/version.py +0 -0
  20. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/SOURCES.txt +0 -0
  21. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/dependency_links.txt +0 -0
  22. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/entry_points.txt +0 -0
  23. {alissa_tools_github_reviewloop-0.5.0 → alissa_tools_github_reviewloop-0.7.0}/src/main/alissa_tools_github_reviewloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-reviewloop
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -154,6 +154,48 @@ class Alissa:
154
154
  # lost its timestamp.
155
155
  return max(found, key=lambda pair: pair[0])[1]
156
156
 
157
+ def count_verdicts(self, task_ref: str) -> int:
158
+ """How many CR6 verdict envelopes are on the review task.
159
+
160
+ One envelope per completed round (CR7, append-only), so this is the
161
+ authoritative round count -- unlike the GitHub review count it cannot be
162
+ thrown off by an empty-bodied review or two reviews in one cycle. Never
163
+ raises: absent, empty, or malformed evidence degrades to 0 (round 1)
164
+ rather than taking the loop down.
165
+ """
166
+ try:
167
+ data = run_json(["alissa", "task", "get", task_ref, "--json"], timeout=90)
168
+ except CommandError as exc:
169
+ log.warning("could not read verdict evidence for %s: %s", task_ref, exc)
170
+ return 0
171
+ except Exception: # pragma: no cover - defence in depth
172
+ log.exception("unexpected failure reading verdict evidence for %s", task_ref)
173
+ return 0
174
+ try:
175
+ return self._count_verdicts(data)
176
+ except Exception: # pragma: no cover - defence in depth
177
+ log.exception("could not count verdict evidence for %s", task_ref)
178
+ return 0
179
+
180
+ @staticmethod
181
+ def _count_verdicts(payload: object) -> int:
182
+ """Count evidence items carrying a verdict envelope. Mirrors
183
+ `_newest_verdict`'s tolerant parsing -- one match per item at most."""
184
+ if not isinstance(payload, dict):
185
+ return 0
186
+ evidence = payload.get("evidence")
187
+ if not isinstance(evidence, list):
188
+ return 0
189
+ count = 0
190
+ for item in evidence:
191
+ if not isinstance(item, dict):
192
+ continue
193
+ for blob in (item.get("title"), item.get("markdownContent")):
194
+ if isinstance(blob, str) and _VERDICT_RE.search(blob):
195
+ count += 1
196
+ break
197
+ return count
198
+
157
199
  def enqueue_reviewer(
158
200
  self,
159
201
  *,
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import logging
14
14
  import re
15
+ import secrets
15
16
  import time
16
17
  from dataclasses import dataclass
17
18
  from enum import Enum
@@ -105,9 +106,17 @@ class Decision:
105
106
 
106
107
 
107
108
  def session_name(pr: PullRequest, round_: int) -> str:
108
- """tmux-safe, unique per (repo, pr, round)."""
109
+ """A tmux-safe reviewer session name, unique per spawn.
110
+
111
+ The `review-<repo>-pr<n>-r<round>` prefix stays human-readable, but a short
112
+ random nonce is appended so a re-used or miscounted round number can never
113
+ collide with a still-live session (a collision wedges the worker -- the
114
+ original 'stuck' failure). Safe to be non-deterministic: the generated name
115
+ is recorded in the spawn ledger and is what gets reaped / self-killed, so the
116
+ daemon never re-derives it.
117
+ """
109
118
  repo = re.sub(r"[^A-Za-z0-9-]", "-", pr.repo).strip("-").lower()
110
- return f"review-{repo}-pr{pr.number}-r{round_}"
119
+ return f"review-{repo}-pr{pr.number}-r{round_}-{secrets.token_hex(3)}"
111
120
 
112
121
 
113
122
  class ReviewWatcher:
@@ -143,18 +152,27 @@ class ReviewWatcher:
143
152
  )
144
153
 
145
154
  my_reviews = self.github.my_reviews(owner, repo, number)
146
- completed = len(my_reviews)
155
+
156
+ # The review task (CR2) is the round record: one verdict envelope per
157
+ # round (CR7), so counting envelopes is the authoritative "rounds
158
+ # completed" -- immune to the GitHub heuristics that drift. A round whose
159
+ # review has an empty body undercounts (round_ repeats -> the session name
160
+ # collides -> the worker wedges); two reviews in one cycle overcount.
161
+ # Fall back to the substantive-review count only before the review task
162
+ # exists (round 1). Looked up here (not in _spawn) because both the count
163
+ # and convergence need it.
164
+ task = self.alissa.find_review_task(owner, repo, number)
165
+ completed = (
166
+ self.alissa.count_verdicts(task.ref) if task is not None
167
+ else len(my_reviews)
168
+ )
147
169
 
148
170
  # Backstop: reap the sessions of rounds that have already landed a review
149
171
  # (the fast path is the reviewer self-killing, but it may forget). A round
150
172
  # <= completed is done; its session, if we still have it, can be killed.
151
173
  self._reap_finished(pr.full_name, number, completed)
152
174
 
153
- # Looked up here rather than inside _spawn: convergence needs the ref
154
- # too. _spawn still handles `task is None` exactly as before.
155
- task = self.alissa.find_review_task(owner, repo, number)
156
-
157
- converged = self._convergence_reason(my_reviews, task)
175
+ converged = self._convergence_reason(my_reviews, task, pr.head_sha)
158
176
  if converged is not None:
159
177
  return Decision(Action.CONVERGED, converged, completed)
160
178
 
@@ -181,7 +199,9 @@ class ReviewWatcher:
181
199
 
182
200
  return self._spawn(pr, round_, task)
183
201
 
184
- def _convergence_reason(self, my_reviews: list[Review], task: Task | None) -> str | None:
202
+ def _convergence_reason(
203
+ self, my_reviews: list[Review], task: Task | None, head_sha: str
204
+ ) -> str | None:
185
205
  """Why the loop is done, or None if it is not.
186
206
 
187
207
  Two independent signals, because neither alone is sufficient:
@@ -195,8 +215,26 @@ class ReviewWatcher:
195
215
  declares this the verdict of record, and unlike the GitHub state it
196
216
  can actually express approval, so it is the signal that closes the
197
217
  loop in practice.
218
+
219
+ BOTH are bound to the current head. An approval means "this code is
220
+ good", so once the implementer pushes past the reviewed commit it is
221
+ about old code and the next round is owed. Without this bind a stale
222
+ approve latches the loop shut forever -- #227: round 1 approved
223
+ `fa304de`, the implementer pushed `fd500fc` and re-requested (and even
224
+ dismissed the approve), yet the envelope still read approve and no round
225
+ 2 was ever queued.
198
226
  """
199
- if my_reviews and my_reviews[-1].state == "APPROVED":
227
+ if not my_reviews:
228
+ return None
229
+
230
+ # New commits since the newest review -> its verdict is about old code.
231
+ # A falsy commit_id (older records lack one) can't be checked, so it
232
+ # falls through rather than blocking convergence.
233
+ newest = my_reviews[-1]
234
+ if newest.commit_id and newest.commit_id != head_sha:
235
+ return None
236
+
237
+ if newest.state == "APPROVED":
200
238
  return "last GitHub review state is APPROVED"
201
239
 
202
240
  # Only checkable once a review task exists; before that there is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-reviewloop
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVIEWLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera