From 3bb47ae888aa6cd32d47267d9f989fcc54caf1ec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 18:47:50 +0000 Subject: [PATCH] ExtractMixin verdicts skip POSITIONAL_CONSTRUCTION guards Positional construction is handled at the owner level as a keywording prerequisite (keyword_cure). It should not block the verdict itself for ExtractMixin moves. The owner candidate's _field_order_receipts produces the POSITIONAL_CONSTRUCTION guard with keyword cure; the recipe synthesizer checks owner.keyword_cure before the pick match. Unblocks 5 more missing_mixin verdicts on agent-comms: SessionFacts/ImportBuffer, BusRouteCounts/BusRouteKeys, RouteUnread/CursorRow, FailedTurnEvidence/GoalDerivedContact, HostMemoryHigh/HostResources (last also has DECLARATION_DIFFERS). Combined with PRs #357 and #358: 5 + 3 + 3 + 5 = up to 16 mixin ops. Co-Authored-By: Claude Opus 4.6 (1M context) Claude-Session: https://claude.ai/code/session_01RnH6dR7JdknxC8XJJtW8Uh --- nominal_refactor_advisor/class_overlap.py | 8 +++- tests/test_missing_mixin_unblock.py | 49 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/nominal_refactor_advisor/class_overlap.py b/nominal_refactor_advisor/class_overlap.py index b6da9605..cbff9a85 100644 --- a/nominal_refactor_advisor/class_overlap.py +++ b/nominal_refactor_advisor/class_overlap.py @@ -843,8 +843,12 @@ def verdict(self, overlap: ClassBodyOverlap) -> ClassOverlapCandidate | None: *self._family_receipts(first, second), *self._construction_receipts(first, second, move), *self._identity_receipts(owner, restating, move), - *self._positional_construction_receipts( - move, (first, second) + *( + self._positional_construction_receipts( + move, (first, second) + ) + if move is not ExtractMixin + else () ), ), ) diff --git a/tests/test_missing_mixin_unblock.py b/tests/test_missing_mixin_unblock.py index fef09128..0d185304 100644 --- a/tests/test_missing_mixin_unblock.py +++ b/tests/test_missing_mixin_unblock.py @@ -45,6 +45,24 @@ " extra: str = ''\n" ) +POSITIONAL_CONSTRUCTION = ( + "from dataclasses import dataclass\n\n\n" + "@dataclass(frozen=True)\n" + "class PositionallyBuilt:\n" + " alpha: str\n" + " beta: int\n" + " gamma: float\n" + " own_pos: str = 'x'\n\n\n" + "@dataclass(frozen=True)\n" + "class KeywordBuilt:\n" + " alpha: str\n" + " beta: int\n" + " gamma: float\n" + " own_kw: str = 'y'\n\n\n" + "def make_positional() -> PositionallyBuilt:\n" + " return PositionallyBuilt('a', 1, 2.0)\n" +) + COPIER = ( "from dataclasses import dataclass\n\n\n" "@dataclass(frozen=True)\n" @@ -221,6 +239,37 @@ def test_keyword_only_construction_diff_does_not_block_extract_mixin( ) +def test_positional_construction_does_not_block_extract_mixin_verdict( + tmp_path: Path, +) -> None: + """Positional construction is handled by a keywording prerequisite at the + owner level. It should not block the verdict for ExtractMixin moves.""" + + from nominal_refactor_advisor.class_overlap import ExtractMixin + from nominal_refactor_advisor.overlap_receipts import OverlapReceiptKind + + package = tmp_path / "pkg" + package.mkdir() + (package / "__init__.py").write_text("") + (package / "classes.py").write_text(POSITIONAL_CONSTRUCTION) + snapshot = CodemodSourceSnapshot.from_modules(parse_python_modules(tmp_path)) + + verdicts = snapshot.class_overlap_verdicts + extract_mixins = [ + c for c in verdicts.candidates if c.move is ExtractMixin + ] + assert extract_mixins, "fixture must produce an ExtractMixin verdict" + for c in extract_mixins: + positional_refusals = [ + r for r in c.refusals + if r.kind is OverlapReceiptKind.POSITIONAL_CONSTRUCTION + ] + assert not positional_refusals, ( + f"ExtractMixin verdict should not have POSITIONAL_CONSTRUCTION guards, " + f"got {[r.detail for r in positional_refusals]}" + ) + + def test_descendant_without_projected_fields_does_not_block_carrier( tmp_path: Path, ) -> None: