Skip to content

feat: implement move refactoring operation - #2074

Closed
vitali87 wants to merge 5 commits into
pr-split/split-pr1549-source/pr-5from
pr-split/split-pr1549-source/pr-6
Closed

vitali87 wants to merge 5 commits into
pr-split/split-pr1549-source/pr-5from
pr-split/split-pr1549-source/pr-6

Conversation

@vitali87

Copy link
Copy Markdown
Owner

Implement move planning, transactional application, and postcondition enforcement.

Files changed

  • codebase_rag/editing/move_operation.py

Diff stats

+372 additions, -0 deletions (372 LOC)

Dependencies

This PR depends on: pr-5

Dependency graph

Merge in this order:

pr-1: feat: add move public API and constants
├── pr-2: feat: parse move spans and imports
│   └── pr-5: feat: retarget move imports safely
│       └── pr-6: feat: implement move refactoring operation  <-- this PR
│           ├── pr-7: feat: expose move through CLI
│           │   └── pr-11: docs: document move operation
│           ├── pr-8: feat: expose move through MCP
│           │   ├── pr-10: test: cover move MCP wiring
│           │   └── pr-11: docs: document move operation
│           └── pr-9: test: cover move operation behavior
│               └── pr-10: test: cover move MCP wiring
├── pr-3: feat: expose import graph sites for move
│   └── pr-5: feat: retarget move imports safely
│       └── pr-6: feat: implement move refactoring operation  <-- this PR
│           ├── pr-7: feat: expose move through CLI
│           │   └── pr-11: docs: document move operation
│           ├── pr-8: feat: expose move through MCP
│           │   ├── pr-10: test: cover move MCP wiring
│           │   └── pr-11: docs: document move operation
│           └── pr-9: test: cover move operation behavior
│               └── pr-10: test: cover move MCP wiring
└── pr-4: feat: detect move contract deltas
    └── pr-5: feat: retarget move imports safely
        └── pr-6: feat: implement move refactoring operation  <-- this PR
            ├── pr-7: feat: expose move through CLI
            │   └── pr-11: docs: document move operation
            ├── pr-8: feat: expose move through MCP
            │   ├── pr-10: test: cover move MCP wiring
            │   └── pr-11: docs: document move operation
            └── pr-9: test: cover move operation behavior
                └── pr-10: test: cover move MCP wiring

@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 8 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: vitali87/code-graph-rag/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 9ffd4b54-a0e0-48bb-863c-24d5249a2aa8

📥 Commits

Reviewing files that changed from the base of the PR and between ad12d8b and 36a2b10.

📒 Files selected for processing (4)
  • codebase_rag/editing/move_operation.py
  • codebase_rag/mcp/tools.py
  • codebase_rag/structural_delta.py
  • codebase_rag/tests/test_variadic_header_detection.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vitali87
vitali87 added this pull request to stack #2081 September 20, 2026 22:56
@vitali87
vitali87 removed this pull request from stack #2081 September 20, 2026 22:56
@vitali87
vitali87 added this pull request to stack #2082 September 20, 2026 22:56
@greptile-apps

greptile-apps Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 2/5

The change is not safe to merge because move operations can leave invalid destination files or revert unrelated edits after a failed postcondition.

Findings

  1. P1 Validate new target files ▶
  2. P1 Reject destination collisions ▶
  3. P1 Undo recorded move ▶

Summary

This change adds graph-backed move planning, importer retargeting, transactional application, and optional graph postcondition enforcement.

A move can commit source text that is invalid for a newly created destination language, and a failed postcondition can undo a later unrelated transaction instead of the move that failed. Destination-name collisions also remain unchecked before a moved top-level definition is appended.

T-Rex validation blocked

The duplicate-destination collision scenario could not complete because the project environment is missing the toml dependency during import initialization; dependency setup is blocked by the unavailable CMake toolchain.

Merge safety

Do not merge until new destination files are validated, postcondition rollback is tied to the recorded transaction, and destination-name collisions are handled.

Reviews (1) · Last reviewed commit: "feat: implement move refactoring operati..."

Comment on lines +312 to +313
tx.stage(report.new_path, new_content)
broken = [key for key, result in results.items() if result.parses is False]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Validate new target files

When the destination does not exist, its generated content is staged directly with tx.stage, but the parse-failure check only covers files returned by patcher.stage_into. Moving Python code to a new path such as pkg/target.go therefore commits unchecked Python text as a Go file. Validate the new destination using its target grammar, or reject incompatible target grammars before committing.

Artifacts

Evidence from the check

  • A minimal executable harness creates a Python source file, moves its function to a new Go destination, asserts the commit succeeds, and checks the destination with gofmt; it is the executed source proving the issue.

Command output from the check

  • Command output captures the exact executed harness and SHA-256 digest; it identifies the source used for the behavioral check.

Command output from the check

  • Captured source excerpt shows `new_content` is staged at line 312 after `patcher.stage_into`, while line 313 checks only Patcher results; it establishes the bypass condition.

Command output from the check

  • Executed harness output records a successful two-file transaction that creates Python content in `pkg/target.go`; gofmt rejects it, proving unchecked invalid destination code was committed.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +156 to +158
patcher.replace_span(
new_path, (len(existing), len(existing)), sep + paste.lstrip("\n")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reject destination collisions

If the destination already defines the same top-level name, this code appends the moved definition without checking for a collision. In Python, the new definition silently replaces the destination module's prior binding at runtime; in languages that reject duplicate declarations, this breaks compilation. Refuse the move or resolve the existing definition before appending.

)
if verdict.ok:
return report._replace(verdict=verdict)
undo_last(self.repo_root)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Undo recorded move

When postcondition enforcement fails, this calls undo_last rather than undoing report.transaction_id. Because the commit lock is released before reingestion and contract verification, another transaction can commit in between. The failure path then reverts that unrelated edit while leaving the invalid move applied. Roll back the recorded move transaction explicitly and report a conflict when it is no longer the newest transaction.

Artifacts

Evidence from the check

  • Executable focused harness that loads the candidate `_enforce_contract` implementation, uses the real transaction/history implementation, forces a failed postcondition after a newer commit, and asserts resulting file/history state.

Command output from the check

  • Successful candidate-path execution. It shows the move remained applied, the newer transaction was reverted, and only the move transaction remained in history.

Command output from the check

  • Successful comparison execution using transaction-ID-targeted undo. It shows refusal because the move was not newest, preserving the newer transaction.

View artifacts

T-Rex Ran code and verified through T-Rex

@greptile-apps

greptile-apps Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P1 New move destination bypasses parse validation ▶

    • Bug
      • Moving a Python definition to a nonexistent explicit .go target commits Python text to that Go file. The focused harness received report.applied=True, found pkg/target.go containing Python code, and gofmt rejected it.
    • Cause
      • For a nonexistent target, planning returns its text as new_content rather than queuing it in Patcher. At line 312 it is staged directly into the transaction. The parse-failure list at line 313 is derived only from patcher.stage_into(tx) results, so it has no parse result for report.new_path.
    • Fix
      • Before committing, parse/format-validate new_content using the destination suffix's grammar (and reject absent/failed validation according to the operation's safety policy), or stage the creation through Patcher so its result participates in the line-313 failure gate.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Planning currently swallows non-“missing file” patch errors (e.g., outside-repo paths) and _add_import can insert before Python docstrings/shebangs, both of which can break correctness.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Implements the core “move refactoring” operation: it plans a move of a definition between modules, stages the required file edits transactionally (cut/paste + import retargeting), and optionally enforces a postcondition via re-ingest + contract verification.

Changes:

  • Added Mover.plan() to compute and stage a move (including cycle refusal and importer retargeting).
  • Added Mover.apply() to commit staged edits via EditTransaction and surface parse-failure rollback.
  • Added contract enforcement path to validate the move’s graph delta and revert on postcondition failure.
File Description
codebase_rag/​editing/​move_operation.py New move operation implementation (planning, patch staging, transactional commit, and postcondition enforcement).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +144 to +159
try:
existing = patcher.source(new_path)
except PatcherError:
new_content = paste.lstrip("\n")
else:
sep = (
""
if not existing.strip()
else ("\n" if existing.endswith(b"\n\n") else "\n\n")
)
if existing and not existing.endswith(b"\n"):
sep = "\n" + sep
patcher.replace_span(
new_path, (len(existing), len(existing)), sep + paste.lstrip("\n")
)
# 3. Importers, and uses through a module import (`pkg.util.helper`).
Comment on lines +271 to +272
at = _import_block_end(source, root)
if language in _JS_LANGUAGES:
Comment on lines +307 to +333
def apply(self, qn: str, target: str, keep_alias: bool = False) -> MoveReport:
report, patcher, new_content = self.plan(qn, target, keep_alias)
tx = EditTransaction(self.repo_root)
results = patcher.stage_into(tx)
if new_content is not None:
tx.stage(report.new_path, new_content)
broken = [key for key, result in results.items() if result.parses is False]
if broken:
tx.rollback()
return report._replace(
message=cs.MOVE_PARSE_FAILED.format(files=", ".join(broken))
)

def verifier(tree: StagedTree) -> VerificationResult | bool | None:
return self.verify(tree) if self.verify is not None else True

outcome = tx.commit(verifier)
report = report._replace(
applied=outcome.applied,
transaction_id=outcome.transaction_id,
files=outcome.files,
diff=outcome.diff,
message=outcome.message,
)
if outcome.applied and self.reingest is not None:
report = self._enforce_contract(report)
return report
Two failures on #2074, unrelated to each other.

`test_every_graph_reader_is_guarded` named `move`. The enum member lands
on this layer ahead of its MCP handler, so nothing dispatches it and it
reads nothing yet. Classified as a non-reader beside RENAME and
CHANGE_SIGNATURE, which e5e9f1c settled: these are graph-driven EDITS
that run their own re-ingest behind the incomplete-run marker, so
refusing up front on a partial graph would block the operation that
repairs it. When the handler lands it routes through `_graph_query` and
must move to `_READS_THE_GRAPH`, or the dispatcher skips its refusal.

`test_extra_positional_to_a_keyword_only_callee_is_too_many`: the
too-many-positionals suppressor counted `kwonlyargs`. Keyword-only
parameters absorb no surplus positional -- CPython raises TypeError --
so a real verdict was dropped. Same fix as #2107, which carries the full
reasoning; the oracle here restated the buggy expression too, so it is
replaced with one that decides by calling the function.
Copilot AI and others added 3 commits September 21, 2026 15:30
Co-authored-by: vitali87 <22404335+vitali87@users.noreply.github.com>
Co-authored-by: vitali87 <22404335+vitali87@users.noreply.github.com>
fix: classify move, and stop kwonly params suppressing TOO_MANY

vitali87 commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded: this is part of the split of #1549, which was closed without merging, and it targets the stale feat/change-signature branch rather than main. The change_signature base was rebuilt on main as #1912 (now stacked as #2161–#2169). The branch is kept, so the move-op work can be rebased onto main if it's picked up again.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants