Skip to content

Commit 34737d2

Browse files
committed
fix: keep sweeper diagnostics nonmutating
1 parent 7e21db1 commit 34737d2

15 files changed

Lines changed: 207 additions & 233 deletions

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_lib/deferred_gap_bookkeeping.py

Lines changed: 58 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -71,32 +71,46 @@ def ensure_observer_discovery_watermark(review_data: dict, surface: str) -> dict
7171
return current
7272

7373

74-
def list_deferred_gap_keys(review_data: dict) -> list[str]:
75-
return list(_deferred_gaps(review_data))
74+
def _observer_now_iso(bot) -> str:
75+
return _now_iso(bot)
76+
77+
78+
def record_observer_watermark_event(bot, review_data: dict, surface: str, event_time: str, event_id: str) -> None:
79+
current = ensure_observer_discovery_watermark(review_data, surface)
80+
current.update(
81+
{
82+
"last_scan_started_at": current.get("last_scan_started_at") or _observer_now_iso(bot),
83+
"last_scan_completed_at": _observer_now_iso(bot),
84+
"last_safe_event_time": event_time,
85+
"last_safe_event_id": event_id,
86+
"lookback_seconds": bot.DEFERRED_DISCOVERY_OVERLAP_SECONDS if hasattr(bot, "DEFERRED_DISCOVERY_OVERLAP_SECONDS") else 3600,
87+
"bootstrap_window_seconds": bot.DEFERRED_DISCOVERY_BOOTSTRAP_WINDOW_SECONDS if hasattr(bot, "DEFERRED_DISCOVERY_BOOTSTRAP_WINDOW_SECONDS") else 604800,
88+
"bootstrap_completed_at": current.get("bootstrap_completed_at") or _observer_now_iso(bot),
89+
}
90+
)
7691

7792

78-
def _deferred_gap_keys(review_data: dict) -> list[str]:
79-
return list_deferred_gap_keys(review_data)
93+
def record_observer_watermark_empty_scan(bot, review_data: dict, surface: str) -> None:
94+
watermark = ensure_observer_discovery_watermark(review_data, surface)
95+
watermark["last_scan_started_at"] = watermark.get("last_scan_started_at") or _observer_now_iso(bot)
96+
watermark["last_scan_completed_at"] = _observer_now_iso(bot)
97+
watermark["bootstrap_completed_at"] = watermark.get("bootstrap_completed_at") or _observer_now_iso(bot)
98+
99+
100+
def list_deferred_gap_keys(review_data: dict) -> list[str]:
101+
return list(_deferred_gaps(review_data))
80102

81103

82104
def get_deferred_gap(review_data: dict, source_event_key: str) -> dict:
83105
gap = _deferred_gaps(review_data).get(source_event_key)
84106
return gap if isinstance(gap, dict) else {}
85107

86108

87-
def _get_deferred_gap(review_data: dict, source_event_key: str) -> dict:
88-
return get_deferred_gap(review_data, source_event_key)
89-
90-
91109
def get_deferred_gap_reason(review_data: dict, source_event_key: str) -> str | None:
92110
reason = get_deferred_gap(review_data, source_event_key).get("reason")
93111
return reason if isinstance(reason, str) else None
94112

95113

96-
def _deferred_gap_reason(review_data: dict, source_event_key: str) -> str | None:
97-
return get_deferred_gap_reason(review_data, source_event_key)
98-
99-
100114
def _now_iso(bot) -> str:
101115
return bot.clock.now().isoformat()
102116

@@ -130,7 +144,9 @@ def clear_deferred_gap(review_data: dict, source_event_key: str) -> bool:
130144
return False
131145

132146

133-
def _clear_source_event_key(review_data: dict, source_event_key: str) -> bool:
147+
def clear_automation_comment_gap(review_data: dict, source_event_key: str) -> bool:
148+
if not source_event_key.startswith("issue_comment:"):
149+
return False
134150
return clear_deferred_gap(review_data, source_event_key)
135151

136152

@@ -144,10 +160,6 @@ def update_deferred_gap_fields(review_data: dict, source_event_key: str, fields:
144160
return previous != existing
145161

146162

147-
def _update_deferred_gap_fields(review_data: dict, source_event_key: str, fields: dict) -> bool:
148-
return update_deferred_gap_fields(review_data, source_event_key, fields)
149-
150-
151163
def mark_reconciled_source_event(
152164
review_data: dict,
153165
source_event_key: str,
@@ -170,35 +182,27 @@ def mark_reconciled_source_event(
170182
return True
171183

172184

173-
def _mark_reconciled_source_event(
174-
review_data: dict,
175-
source_event_key: str,
176-
*,
177-
reconciled_at: str | None = None,
178-
) -> bool:
179-
return mark_reconciled_source_event(
180-
review_data,
181-
source_event_key,
182-
reconciled_at=reconciled_at,
183-
)
184-
185-
186185
def was_reconciled_source_event(review_data: dict, source_event_key: str) -> bool:
187186
return _is_valid_reconciled_source_event(
188187
_reconciled_source_events(review_data).get(source_event_key),
189188
source_event_key,
190189
)
191190

192191

193-
def _was_reconciled_source_event(review_data: dict, source_event_key: str) -> bool:
194-
return was_reconciled_source_event(review_data, source_event_key)
195-
196-
197192
def _payload_or_existing(payload: dict, existing: dict, key: str):
198193
value = payload.get(key)
199194
return existing.get(key) if value is None else value
200195

201196

197+
def _source_event_created_at(payload: dict, existing: dict):
198+
return (
199+
payload.get("source_created_at")
200+
or payload.get("source_submitted_at")
201+
or payload.get("source_dismissed_at")
202+
or existing.get("source_event_created_at")
203+
)
204+
205+
202206
def record_deferred_gap_diagnostic(
203207
bot,
204208
review_data: dict,
@@ -216,43 +220,26 @@ def record_deferred_gap_diagnostic(
216220
if not isinstance(existing, dict):
217221
existing = {}
218222
previous = deepcopy(existing)
219-
existing.update(
220-
{
221-
"source_event_key": source_event_key,
222-
"source_event_kind": f"{payload.get('source_event_name')}:{payload.get('source_event_action')}",
223-
"pr_number": payload.get("pr_number"),
224-
"reason": reason,
225-
"source_event_created_at": payload.get("source_created_at") or payload.get("source_submitted_at"),
226-
"source_run_id": _payload_or_existing(payload, existing, "source_run_id"),
227-
"source_run_attempt": _payload_or_existing(payload, existing, "source_run_attempt"),
228-
"source_workflow_file": _payload_or_existing(payload, existing, "source_workflow_file"),
229-
"source_artifact_name": _payload_or_existing(payload, existing, "source_artifact_name"),
230-
"first_noted_at": existing.get("first_noted_at") or _now_iso(bot),
231-
"last_checked_at": _now_iso(bot),
232-
"operator_action_required": True,
233-
"diagnostic_summary": diagnostic_summary,
234-
"failure_kind": failure_kind,
235-
}
236-
)
223+
fields = {
224+
"source_event_key": source_event_key,
225+
"source_event_kind": f"{payload.get('source_event_name')}:{payload.get('source_event_action')}",
226+
"pr_number": payload.get("pr_number"),
227+
"reason": reason,
228+
"source_event_created_at": _source_event_created_at(payload, existing),
229+
"source_run_id": _payload_or_existing(payload, existing, "source_run_id"),
230+
"source_run_attempt": _payload_or_existing(payload, existing, "source_run_attempt"),
231+
"source_workflow_file": _payload_or_existing(payload, existing, "source_workflow_file"),
232+
"source_artifact_name": _payload_or_existing(payload, existing, "source_artifact_name"),
233+
"first_noted_at": existing.get("first_noted_at") or _now_iso(bot),
234+
"last_checked_at": _now_iso(bot),
235+
"operator_action_required": True,
236+
"diagnostic_summary": diagnostic_summary,
237+
"failure_kind": failure_kind,
238+
}
239+
source_dismissed_at = _payload_or_existing(payload, existing, "source_dismissed_at")
240+
if source_dismissed_at is not None:
241+
fields["source_dismissed_at"] = source_dismissed_at
242+
existing.update(fields)
237243
changed = previous != existing
238244
deferred_gaps[source_event_key] = existing
239245
return changed
240-
241-
242-
def _update_deferred_gap(
243-
bot,
244-
review_data: dict,
245-
payload: dict,
246-
reason: str,
247-
diagnostic_summary: str,
248-
*,
249-
failure_kind: str | None = None,
250-
) -> bool:
251-
return record_deferred_gap_diagnostic(
252-
bot,
253-
review_data,
254-
payload,
255-
reason,
256-
diagnostic_summary,
257-
failure_kind=failure_kind,
258-
)

scripts/reviewer_bot_lib/maintenance.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,7 @@ def handle_manual_dispatch(bot, state: dict) -> bool:
9393
bot.collect_touched_item(issue_number)
9494
return False
9595
if action == "check-overdue":
96-
result = maintenance_schedule.handle_scheduled_check_result(bot, state)
97-
for issue_number in result.touched_items:
98-
bot.collect_touched_item(issue_number)
99-
return result.state_changed
96+
raise RuntimeError("manual check-overdue must be dispatched through handle_scheduled_check_result")
10097
if action == "execute-pending-privileged-command":
10198
source_event_key = request.privileged_source_event_key
10299
if not source_event_key:

scripts/reviewer_bot_lib/sweeper.py

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,9 @@
1414
from . import sweeper_observer_correlation as observer_correlation
1515
from .config import REVIEW_FRESHNESS_RUNBOOK_PATH
1616
from .review_state import (
17-
accept_reviewer_review_from_live_review,
1817
get_current_cycle_boundary,
19-
record_reviewer_activity,
20-
refresh_reviewer_review_from_live_preferred_review,
2118
semantic_key_seen,
2219
)
23-
from .reviews import rebuild_pr_approval_state
2420
from .runtime_protocols import SweeperContext
2521

2622

@@ -93,7 +89,6 @@ def _sleep(bot: SweeperContext, seconds: float) -> None:
9389
_fetch_workflow_runs_for_file = observer_correlation.fetch_workflow_runs_for_file
9490
_fetch_run_detail = observer_correlation.fetch_run_detail
9591
inspect_run_artifact_payloads = observer_correlation.inspect_run_artifact_payloads
96-
_update_observer_watermark = observer_correlation.update_observer_watermark
9792

9893

9994
def _complete_surface_scan(bot, review_data: dict, surface: str, discovered: list[dict]) -> None:
@@ -102,12 +97,9 @@ def _complete_surface_scan(bot, review_data: dict, surface: str, discovered: lis
10297
review_data,
10398
surface,
10499
discovered,
105-
_load_surface_watermark,
106100
)
107101

108102

109-
110-
111103
def _diagnose_deferred_event(
112104
bot,
113105
review_data: dict,
@@ -263,7 +255,7 @@ def _purge_bot_authored_comment_gap(bot, review_data: dict, source_event_key: st
263255
return False
264256
if source_event_key not in gap_bookkeeping.list_deferred_gap_keys(review_data):
265257
return False
266-
return gap_bookkeeping.clear_deferred_gap(review_data, source_event_key)
258+
return gap_bookkeeping.clear_automation_comment_gap(review_data, source_event_key)
267259

268260

269261
def _maybe_fetch_single_candidate_run_detail(bot, run_correlation: dict, artifact_correlation: dict | None) -> dict | None:
@@ -284,33 +276,6 @@ def _maybe_fetch_single_candidate_run_detail(bot, run_correlation: dict, artifac
284276
return _fetch_run_detail(bot, run_id)
285277

286278

287-
def _repair_visible_review_gap(bot, review_data: dict, issue_number: int, source_event_key: str, review: dict) -> bool:
288-
repair = deferred_gap_diagnosis.recommend_review_submission_gap_repair(
289-
review_data,
290-
review,
291-
source_event_key,
292-
artifact_status=None,
293-
current_cycle_boundary=get_current_cycle_boundary(bot, review_data),
294-
)
295-
if repair is None:
296-
return False
297-
payload = repair["payload"]
298-
author = str(payload["author"])
299-
submitted_at = str(payload["submitted_at"])
300-
changed = accept_reviewer_review_from_live_review(review_data, review, actor=author)
301-
changed = refresh_reviewer_review_from_live_preferred_review(
302-
bot,
303-
issue_number,
304-
review_data,
305-
actor=author,
306-
)[0] or changed
307-
record_reviewer_activity(review_data, submitted_at)
308-
completion, _ = rebuild_pr_approval_state(bot, issue_number, review_data)
309-
reconciled_changed = gap_bookkeeping.mark_reconciled_source_event(review_data, source_event_key)
310-
gap_cleared_changed = gap_bookkeeping.clear_deferred_gap(review_data, source_event_key)
311-
return changed or completion is not None or reconciled_changed or gap_cleared_changed
312-
313-
314279
def _discover_visible_comment_events(bot, issue_number: int, review_data: dict) -> tuple[list[dict] | None, bool]:
315280
watermark = _load_surface_watermark(review_data, "comments")
316281
watermark["last_scan_started_at"] = _now_iso()
@@ -585,22 +550,13 @@ def sweep_deferred_gaps(bot, state: dict) -> bool:
585550
run_detail = _maybe_fetch_single_candidate_run_detail(bot, run_correlation, artifact_correlation)
586551
review_payload = discovered.get("review") if isinstance(discovered.get("review"), dict) else None
587552
artifact_status = artifact_correlation.get("status") if isinstance(artifact_correlation, dict) else None
588-
repair_recommendation = deferred_gap_diagnosis.recommend_review_submission_gap_repair(
553+
visible_review_diagnostic = deferred_gap_diagnosis.describe_review_submission_gap_diagnostic(
589554
review_data,
590555
review_payload,
591556
source_event_key,
592557
artifact_status=artifact_status,
593558
current_cycle_boundary=get_current_cycle_boundary(bot, review_data),
594559
)
595-
if repair_recommendation is not None and _repair_visible_review_gap(
596-
bot,
597-
review_data,
598-
issue_number,
599-
source_event_key,
600-
review_payload,
601-
):
602-
changed = True
603-
continue
604560
reason, diagnostic_reason = deferred_gap_diagnosis.evaluate_deferred_gap_state(
605561
{
606562
**existing_gap,
@@ -611,6 +567,8 @@ def sweep_deferred_gaps(bot, state: dict) -> bool:
611567
artifact_correlation,
612568
runbook_signature=_approval_pending_signature_from_runbook(),
613569
)
570+
if visible_review_diagnostic is not None:
571+
diagnostic_reason = f"{diagnostic_reason}; {visible_review_diagnostic['category']}"
614572
_record_gap_diagnostics(
615573
bot,
616574
review_data,
@@ -626,15 +584,17 @@ def sweep_deferred_gaps(bot, state: dict) -> bool:
626584
reason=reason,
627585
diagnostic_reason=diagnostic_reason,
628586
)
587+
if visible_review_diagnostic is not None:
588+
gap_bookkeeping.update_deferred_gap_fields(
589+
review_data,
590+
source_event_key,
591+
{"visible_review_diagnostic": visible_review_diagnostic},
592+
)
629593
changed = True
630594
if discovered_reviews:
631-
last_review = discovered_reviews[-1]
632-
_update_observer_watermark(bot, review_data, "reviews_submitted", last_review["source_created_at"], last_review["object_id"])
595+
_complete_surface_scan(bot, review_data, "reviews_submitted", discovered_reviews)
633596
else:
634-
watermark = _load_surface_watermark(review_data, "reviews_submitted")
635-
watermark["last_scan_started_at"] = watermark.get("last_scan_started_at") or _now_iso()
636-
watermark["last_scan_completed_at"] = _now_iso()
637-
watermark["bootstrap_completed_at"] = watermark.get("bootstrap_completed_at") or _now_iso()
597+
gap_bookkeeping.record_observer_watermark_empty_scan(bot, review_data, "reviews_submitted")
638598
discovered_review_comments, review_comments_complete = _discover_visible_review_comment_events(bot, issue_number, review_data)
639599
if review_comments_complete and isinstance(discovered_review_comments, list):
640600
for discovered in discovered_review_comments:

0 commit comments

Comments
 (0)