feat(controller): Phase 4 metadata-hygiene + round-2 adversarial fixes
Five deterministic, idempotent Phase 4 checks: 1. completed_not_closed — close linked issues on MERGED 2. closing_keyword_fixup — add Closes #N to PR bodies 3. label_sync_from_issue — copy Priority/Type/MoSCoW labels 4. state_label_inference — sync State/* label to current_state 5. milestone_assignment — copy milestone from linked issue All default-off via CONTROLLER_METADATA_HYGIENE_ENABLED + per-check granular env flags. Dry-run mode shares the grooming CONTROLLER_GROOMING_DRY_RUN flag. Round-1 fixes (applied before this commit): - False-positive idempotency lock (executed=True on skip) - Unbounded MERGED scan → LEFT JOIN candidate query - Duplicate _classify_forgejo_status → import from forgejo_writes - Bare-ref regex too broad ([#42](url) misread) → add [ lookbehind - Wrong audit stage → 'metadata_hygiene' Round-2 adversarial fixes (3 architect, 4 principal, 7 test engineer): - completed_not_closed: executed=True only when ALL refs close; partial success writes executed=False so remaining issues retry - milestone_assignment: was calling get_pr_details (hits /pulls/, returns 404 for plain issues) → now uses get_issue_state (/issues/{n}) so milestone fetch works for all issue types - label_sync failure path: write executed=False audit row for observability; pre-fix left no audit trail for persistent failures - _BARE_REF_RE: add ( to lookbehind to exclude (#42) link destinations - state_label_inference: re-read current_state inside inner session to avoid stale-snapshot spurious label writes across session boundaries - _last_synced_state: add decision_id DESC tiebreaker for same-second wall-clock rows - dry-run completed_not_closed: separate early-return path to avoid inflating completed_not_closed_executed counter 71 tests (54 round-1 + 17 round-2): - TestCompletedNotClosedPartialSuccess (3) — partial/zero/full success - TestLabelSyncAdjustLabelsFailure (2) — failure audit + retry - TestStateLabelAdjustLabelsFailure (2) — no executed=1 on failure - TestStateLabelInferenceTerminalWorkflows (3) — MERGED/ABANDONED sync - TestLastSyncedStateDryRunThenReal (2) — dry-run → real-run - TestClosingKeywordFixupBareRefAlreadyCovered (2) — candidates subtraction - TestErrorPathHandlingRound2 (3) — label_sync + state_label errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,11 @@ from .grooming_side_effects import (
|
||||
run_grooming_side_effects_tick,
|
||||
)
|
||||
from .merging import MergeCallback, MergingHandlerReport, run_merging_tick
|
||||
from .metadata_hygiene import (
|
||||
MetadataHygieneCallbacks,
|
||||
MetadataHygieneReport,
|
||||
run_metadata_hygiene_tick,
|
||||
)
|
||||
from .reviewer_abandon_side_effects import (
|
||||
ReviewerAbandonCallbacks,
|
||||
ReviewerAbandonSideEffectReport,
|
||||
@@ -237,6 +242,13 @@ def master_main_loop(
|
||||
# and performs the Forgejo close via ``forgejo_writes.close_act``
|
||||
# with ``cause=Cause.REVIEWER_ABANDON``. Same shape + dry_run
|
||||
# source as the estimator-abandon tick — Phase 3 (2026-05-25).
|
||||
metadata_hygiene_callbacks: MetadataHygieneCallbacks | None = None,
|
||||
# Phase 4 (2026-05-25): when set, the metadata-hygiene tick runs
|
||||
# every iteration; it invokes each enabled check (completed-not-
|
||||
# closed, closing-keyword fixup, label sync, state-label inference,
|
||||
# milestone assignment). Each check is independently gated via
|
||||
# per-check flags on the callbacks dataclass. None disables the
|
||||
# entire phase regardless of per-check flags.
|
||||
grooming_callbacks: GroomingCallbacks | None = None,
|
||||
# When set, the grooming side-effect tick fires every iteration:
|
||||
# it finds workflows whose state-machine just transitioned via
|
||||
@@ -474,6 +486,25 @@ def master_main_loop(
|
||||
"reviewer_abandon_side_effects tick raised; continuing"
|
||||
)
|
||||
|
||||
# Phase 4 (2026-05-25) — metadata-hygiene dispatcher.
|
||||
# Runs every iteration; each of the five checks
|
||||
# (completed-not-closed, closing-keyword fixup, label sync,
|
||||
# state-label inference, milestone assignment) is gated by
|
||||
# its own per-check flag on ``metadata_hygiene_callbacks``.
|
||||
# ``None`` disables the whole phase regardless of per-check
|
||||
# flags. Cheap when no candidates exist per check.
|
||||
metadata_hygiene_report: MetadataHygieneReport | None = None
|
||||
if metadata_hygiene_callbacks is not None:
|
||||
try:
|
||||
metadata_hygiene_report = run_metadata_hygiene_tick(
|
||||
engine=engine,
|
||||
callbacks=metadata_hygiene_callbacks,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"metadata_hygiene tick raised; continuing"
|
||||
)
|
||||
|
||||
# Phase 1k+++ (real-run): MERGING handler. For workflows
|
||||
# in MERGING state, call the Forgejo merge endpoint via
|
||||
# the injected callback. Without this, workflows that
|
||||
|
||||
Reference in New Issue
Block a user