363bcfad61
dispatch_implementer.py is 3225 lines — well over the ~500-line per- module budget the rest of tools/ honours. C2 is the long-overdue decomposition; this commit starts the work with the smallest / cleanest extraction available: the pure G1 metadata-only classifier (no module state, single public surface) moves to a new sibling _implementer_metadata_classifier.py, loaded through the existing _loader.load_sibling machinery just like every other helper in this file. The leading-underscore alias dispatch_implementer._classify_metadata_only re-exports the function so the G1 tests (and any other call sites) keep working unchanged. The extraction is a pure refactor — the full 1754-test auto-agents suite passes unchanged. Subsequent C2 steps (extract _post_session_action_with_escalation — the ~350-line nested loop the harvest plan singled out by name — plus prompt-assembly + short-circuit blocks) are larger pieces that warrant fresh-context attention. This commit establishes the pattern (new module, _load_sibling line, leading-underscore alias) for those follow-ups. Refs: docs/development/final-working-harvest-plan.md (C2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
3.5 KiB
Python
81 lines
3.5 KiB
Python
"""Metadata-only diversion classifier.
|
|
|
|
Extracted from :mod:`dispatch_implementer` (C2 harvest 2026-05-15)
|
|
as the first cohesive unit moved out of the 3.2k-line driver toward
|
|
the ~500-line per-module budget the rest of ``tools/`` honours.
|
|
|
|
The function is pure: takes a Forgejo item dict + an
|
|
``ImplementerPrefetchResult``-shaped carrier, returns a strict
|
|
``bool``. No I/O, no LLM, no module-level state — the natural
|
|
candidate for the first extraction.
|
|
|
|
Re-exported through ``dispatch_implementer._classify_metadata_only``
|
|
for backwards-compatibility with the G1 tests, so test imports and
|
|
call sites land in one place.
|
|
|
|
Caller pattern: see the inline call in ``_prefetch_prompt`` —
|
|
result is stashed on the per-item context as
|
|
``metadata_only_candidate`` for cycle telemetry and any future
|
|
diversion path to consume.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def classify_metadata_only(item: dict[str, Any], prefetch: Any) -> bool:
|
|
"""G1 harvest (2026-05-15) — return ``True`` when this work item
|
|
appears to be **metadata-only** (no code work required), based on
|
|
deterministic signals already in the prefetch carrier. Returns
|
|
``False`` whenever any signal hints at code work or any signal is
|
|
ambiguous: the harvest plan's explicit tie-breaker is "when in
|
|
doubt, classify as code work — the cost of an unnecessary
|
|
code-work attempt is much lower than incorrectly skipping
|
|
legitimate implementation."
|
|
|
|
Signals consulted (all from the carrier; no new network calls):
|
|
|
|
1. ``item`` must be PR-shaped — ``new_issue`` work always
|
|
requires a fresh implementation, so it is never metadata-only.
|
|
2. CI overall state must NOT be ``failure`` or ``error``. A
|
|
failing CI is by definition something the worker has to fix.
|
|
3. Per-check ``ci_detail`` rows must NOT include any
|
|
``failure`` / ``error`` status, even if the overall combined
|
|
state happens to be ``success`` (defensive against Forgejo
|
|
race windows where the combined state lags an individual
|
|
check).
|
|
4. No active ``request_changes_reviews``: a REQUEST_CHANGES
|
|
review is feedback the worker must respond to. (A label-only
|
|
request would be a code review smell on the reviewer's part,
|
|
so we conservatively assume any RC review references source.)
|
|
|
|
When all four hold the item is a candidate for diversion to a
|
|
grooming-style path. The actual no-clone handling is wired
|
|
separately and may be deferred — this classifier ships first so
|
|
an operator can observe how often the diversion would fire on
|
|
real PRs before any behaviour change.
|
|
|
|
Pure function; safe to call from anywhere in the dispatch path.
|
|
"""
|
|
if not isinstance(item.get("head"), dict):
|
|
return False
|
|
if prefetch is None:
|
|
return False
|
|
ci_status = getattr(prefetch, "ci_status", None) or {}
|
|
if isinstance(ci_status, dict):
|
|
state = str(ci_status.get("state") or "").lower()
|
|
if state in ("failure", "error"):
|
|
return False
|
|
ci_detail = getattr(prefetch, "ci_detail", None) or []
|
|
if isinstance(ci_detail, list):
|
|
for check in ci_detail:
|
|
if not isinstance(check, dict):
|
|
continue
|
|
status = str(check.get("status") or "").lower()
|
|
if status in ("failure", "error"):
|
|
return False
|
|
rc_reviews = getattr(prefetch, "request_changes_reviews", None) or []
|
|
if isinstance(rc_reviews, list) and len(rc_reviews) > 0:
|
|
return False
|
|
return True
|