diff --git a/lib/substantive_fixer.py b/lib/substantive_fixer.py index 9fa33c3..4d6dcd5 100644 --- a/lib/substantive_fixer.py +++ b/lib/substantive_fixer.py @@ -539,6 +539,7 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]: # Filter to only PRs with substantive issues (not just mechanical) substantive_rows = [] + skipped_no_tags = [] for row in rows: try: issues = json.loads(row["eval_issues"] or "[]") @@ -546,8 +547,20 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]: continue if set(issues) & (FIXABLE_TAGS | CONVERTIBLE_TAGS | UNFIXABLE_TAGS): substantive_rows.append(row) + else: + skipped_no_tags.append((row["number"], issues)) if not substantive_rows: + # Visibility for the LIMIT-3 head-of-line block: if the oldest + # candidates have no fixer-actionable tags (e.g. eval_issues=[], + # broken_wiki_links only), the cycle silently returns 0 — and the + # next cycle picks the same head-of-line, forever. Log the eval_issues + # of skipped candidates so the journal makes the block visible. + if skipped_no_tags: + logger.info( + "Substantive fix cycle: 0 actionable from %d candidate(s) — head-of-line: %s", + len(rows), skipped_no_tags, + ) return 0, 0 fixed = 0 @@ -559,7 +572,13 @@ async def substantive_fix_cycle(conn, max_workers=None) -> tuple[int, int]: if result.get("action"): fixed += 1 elif result.get("skipped"): - logger.debug("PR #%d: substantive fix skipped: %s", row["number"], result.get("reason")) + # Was DEBUG — promoted to INFO to make stuck-PR root cause + # visible without enabling DEBUG fleet-wide. (Ship Apr 24+ + # silent skip diagnosis.) + logger.info( + "PR #%d: substantive fix skipped: %s", + row["number"], result.get("reason"), + ) except Exception: logger.exception("PR #%d: substantive fix failed", row["number"]) errors += 1