Skip to content

Commit 0934126

Browse files
authored
refactor: close retained reviewer residue (#565)
* refactor: close retained reviewer residue * fix: require exact review dismissal time * test: align closed arrival expectations * fix: suppress closed review replays * fix: close reviewer cleanup gaps * fix: tighten reviewer cleanup ownership * fix: harden reviewer cleanup metadata * fix: guard manual reviewer cleanup * fix: keep sweeper diagnostics nonmutating * refactor: tighten deferred gap ownership * refactor: simplify deferred maintenance boundaries
1 parent 4c1f75f commit 0934126

37 files changed

Lines changed: 1817 additions & 404 deletions

docs/reviewer-bot-review-freshness-operator-runbook.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,5 @@ Do not emit `artifact_expired` unless prior visibility or documented retention p
108108
- Missing runs are not proof that nothing happened.
109109
- Operator-visible workflow summaries should point to this file path: `docs/reviewer-bot-review-freshness-operator-runbook.md`.
110110
- Same-repo trusted-direct PR comment handling is limited to repo-associated `User` principals with `OWNER`, `MEMBER`, or `COLLABORATOR` association in the dedicated trusted workflow.
111-
- Non-human automation PR comments, reviewer-bot self-comments, cross-repo PR comments, Dependabot PR comments, and all PR review freshness events stay deferred or ignored in this rollout.
111+
- Non-human automation PR comments, reviewer-bot self-comments, cross-repo PR comments, Dependabot PR comments, and PR review freshness events are handled only by trusted observer and reconcile paths in this rollout.
112+
- Deferred review dismissal replay requires exact source time from the observer payload or an exact `review_dismissed` timeline event matched by review id; otherwise it remains diagnostic-only and must not be replayed manually.

scripts/reviewer_bot_core/deferred_gap_diagnosis.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def evaluate_deferred_gap_state(
259259
return "artifact_invalid", str(artifact_correlation.get("reason") or "artifact_invalid")
260260

261261

262-
def recommend_visible_review_repair(
262+
def describe_visible_review_submission(
263263
review_data: dict,
264264
review: dict,
265265
source_event_key: str,
@@ -284,10 +284,14 @@ def recommend_visible_review_repair(
284284
return None
285285
if source_event_key != f"pull_request_review:{review_id}":
286286
return None
287-
return author, submitted_at, commit_id
287+
return {
288+
"author": author,
289+
"submitted_at": submitted_at,
290+
"commit_id": commit_id,
291+
}
288292

289293

290-
def recommend_review_submission_gap_repair(
294+
def describe_review_submission_gap_diagnostic(
291295
review_data: dict,
292296
review: dict | None,
293297
source_event_key: str,
@@ -297,20 +301,15 @@ def recommend_review_submission_gap_repair(
297301
) -> dict[str, object] | None:
298302
if review is None or artifact_status == "exact_artifact_match":
299303
return None
300-
repair = recommend_visible_review_repair(
304+
visible_review = describe_visible_review_submission(
301305
review_data,
302306
review,
303307
source_event_key,
304308
current_cycle_boundary=current_cycle_boundary,
305309
)
306-
if repair is None:
310+
if visible_review is None:
307311
return None
308-
author, submitted_at, commit_id = repair
309312
return {
310-
"category": "review_submission_repair",
311-
"payload": {
312-
"author": author,
313-
"submitted_at": submitted_at,
314-
"commit_id": commit_id,
315-
},
313+
"category": "visible_review_without_replay_artifact",
314+
"payload": visible_review,
316315
}

scripts/reviewer_bot_core/state_adapters.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def ensure_sidecar_subtree(review_entry: dict[str, Any], *, state_last_updated:
103103
sidecars = {}
104104
review_entry["sidecars"] = sidecars
105105

106+
# Codec boundary exception: runtime sidecar mutation belongs to
107+
# deferred_gap_bookkeeping, but persisted legacy shapes are normalized here.
106108
sidecars["pending_privileged_commands"] = (
107109
deepcopy(sidecars.get("pending_privileged_commands"))
108110
if isinstance(sidecars.get("pending_privileged_commands"), dict)

scripts/reviewer_bot_lib/app.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def execute_run(bot: AppExecutionRuntime, context: EventContext) -> ExecutionRes
150150
sync_changes: list[str] = []
151151
restored: list[str] = []
152152
loaded_active_reviews_count = 0
153+
loaded_active_review_numbers: set[int] = set()
153154
touched_items: list[int] = []
154155
projection_failure: RuntimeError | None = None
155156
loaded_epoch: str | None = None
@@ -186,6 +187,11 @@ def execute_run(bot: AppExecutionRuntime, context: EventContext) -> ExecutionRes
186187
active_reviews = state.get("active_reviews")
187188
if isinstance(active_reviews, dict):
188189
loaded_active_reviews_count = len(active_reviews)
190+
loaded_active_review_numbers = {
191+
int(issue_key)
192+
for issue_key in active_reviews
193+
if isinstance(issue_key, str) and issue_key.isdigit()
194+
}
189195
loaded_epoch = state.get("freshness_runtime_epoch") if isinstance(state.get("freshness_runtime_epoch"), str) else None
190196

191197
if lock_required:
@@ -241,7 +247,11 @@ def execute_run(bot: AppExecutionRuntime, context: EventContext) -> ExecutionRes
241247
state_changed = bot.handlers.handle_comment_event(state)
242248

243249
elif event_name == "workflow_dispatch":
244-
state_changed = bot.handlers.handle_manual_dispatch(state)
250+
if maintenance.is_schedule_like_manual_action(context.manual_action):
251+
schedule_result = bot.handlers.handle_manual_dispatch_result(state)
252+
state_changed = schedule_result.state_changed
253+
else:
254+
state_changed = bot.handlers.handle_manual_dispatch(state)
245255

246256
elif event_name == "schedule":
247257
schedule_result = bot.handlers.handle_scheduled_check_result(state)
@@ -289,21 +299,28 @@ def execute_run(bot: AppExecutionRuntime, context: EventContext) -> ExecutionRes
289299
"Acquire lock before mutating state."
290300
)
291301

292-
if event_name == "schedule":
302+
if schedule_result is not None:
293303
current_active_reviews = state.get("active_reviews")
294304
current_active_reviews_count = (
295305
len(current_active_reviews) if isinstance(current_active_reviews, dict) else 0
296306
)
297307
allow_empty_override = (
298308
bot.get_config_value("ALLOW_EMPTY_ACTIVE_REVIEWS_WRITE").strip().lower() == "true"
299309
)
310+
allow_closed_cleanup_empty = (
311+
schedule_result is not None
312+
and loaded_active_reviews_count == len(loaded_active_review_numbers)
313+
and bool(loaded_active_review_numbers)
314+
and set(schedule_result.closed_cleanup_removed_items) == loaded_active_review_numbers
315+
)
300316
if (
301317
loaded_active_reviews_count > 0
302318
and current_active_reviews_count == 0
303319
and not allow_empty_override
320+
and not allow_closed_cleanup_empty
304321
):
305322
raise RuntimeError(
306-
"STATE_GUARD_BLOCKED_EMPTY_ACTIVE_REVIEWS: refusing to persist schedule "
323+
"STATE_GUARD_BLOCKED_EMPTY_ACTIVE_REVIEWS: refusing to persist maintenance "
307324
f"state update that drops active_reviews from {loaded_active_reviews_count} "
308325
"to 0. Set ALLOW_EMPTY_ACTIVE_REVIEWS_WRITE=true to override."
309326
)

scripts/reviewer_bot_lib/bootstrap_runtime.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ def handle_comment_event(self, current_state):
187187
def handle_manual_dispatch(self, current_state):
188188
return maintenance.handle_manual_dispatch(self._runtime_getter(), current_state)
189189

190+
def handle_manual_dispatch_result(self, current_state):
191+
return maintenance.handle_manual_dispatch_result(self._runtime_getter(), current_state)
192+
190193
def handle_scheduled_check_result(self, current_state):
191194
return maintenance.handle_scheduled_check_result(self._runtime_getter(), current_state)
192195

scripts/reviewer_bot_lib/comment_routing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ def handle_comment_event(
182182
issue_number = comment_request.issue_number
183183
if not issue_number:
184184
return False
185-
bot.collect_touched_item(issue_number)
186185
route = _route_issue_comment_trust(
187186
bot,
188187
comment_request,
@@ -192,20 +191,21 @@ def handle_comment_event(
192191
return False
193192
if route == comment_routing_policy.ProcessingTarget.ISSUE_DIRECT:
194193
if comment_request.issue_state == "closed":
195-
removed = state.get("active_reviews", {}).pop(str(issue_number), None)
196194
_log(
197195
bot,
198196
"info",
199197
f"Ignoring direct comment on closed issue #{issue_number}",
200198
issue_number=issue_number,
201199
issue_state=comment_request.issue_state,
202200
)
203-
return removed is not None
201+
return False
202+
bot.collect_touched_item(issue_number)
204203
return _process_comment_event(bot, state, comment_request)
205204
if route == PrCommentRouterOutcome.TRUSTED_DIRECT:
206205
if comment_request.issue_state != "open":
207206
return False
208207
if not _require_v18_for_pr(bot, state, comment_request, "pr_trusted_direct_comment"):
209208
return False
209+
bot.collect_touched_item(issue_number)
210210
return _process_comment_event(bot, state, comment_request)
211211
raise RuntimeError("Deferred PR comment events must not mutate directly in trusted workflows")

0 commit comments

Comments
 (0)