Repository navigation
feat: expose change-signature via API, CLI, and MCP #2044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| """`change_signature(qn, new_params)`: edit-algebra operation 2 (issue #1533). | ||
|
|
||
|
|
||
| The definition changes, its callers in other packages do not: that is the | ||
| most-cited cross-package breakage. The graph knows every call site with | ||
| its argument shape (issue #1522), how each edge was resolved (issue #1526) | ||
| and the declared parameter types (issue #1527), so every site can be | ||
| rewritten per an explicit mapping or listed as unmapped. | ||
|
|
||
| `new_params` describes the new parameter list in order. Each entry says | ||
| where its value comes from at a call site: an old positional index | ||
| (`from_index`, receiver excluded), an old parameter name (`from_name`), | ||
| a default literal (`literal`, inserted where the site passes nothing), or | ||
| nothing at all (unmapped: sites that pass no value are left untouched and | ||
| listed). Every definition in the override hierarchy is rewritten too. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Callable, Iterable | ||
| from pathlib import Path | ||
|
|
||
| from .. import constants as cs | ||
| from ..graph_query import QueryFn | ||
| from .contract import Reingest, change_signature_expectation, measure, verify | ||
| from .signature_planner import SignaturePlanner | ||
| from .signature_types import ( | ||
| ParamSpec, | ||
| RewrittenSite, | ||
| SignatureRefused, | ||
| SignatureReport, | ||
| UnmappedSite, | ||
| parse_param_spec, | ||
| sites_for, | ||
| ) | ||
| from .transaction import EditTransaction, StagedTree, VerificationResult, undo_last | ||
|
|
||
|
|
||
| class SignatureChanger(SignaturePlanner): | ||
| def apply( | ||
| self, qn: str, specs: Iterable[ParamSpec], allow_heuristic: bool = False | ||
| ) -> SignatureReport: | ||
| # The contract must know the caller waived the guess check, or it | ||
| # rolls back every rewrite of a heuristic site it was asked for. | ||
| self._allow_heuristic = allow_heuristic | ||
| report, patcher = self.plan(qn, specs, allow_heuristic) | ||
| tx = EditTransaction(self.repo_root) | ||
| results = patcher.stage_into(tx) | ||
| broken = [key for key, result in results.items() if result.parses is False] | ||
| if broken: | ||
| tx.rollback() | ||
| return report._replace( | ||
| files=tuple(sorted(results)), | ||
| message=cs.SIGNATURE_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 | ||
|
|
||
| def _enforce_contract(self, report: SignatureReport) -> SignatureReport: | ||
| assert self.reingest is not None | ||
| delta = measure( | ||
| self.fetch_all, self.project, self.repo_root, report.files, self.reingest | ||
| ) | ||
|
Comment on lines
+73
to
+75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The edit is committed before |
||
| verdict = verify( | ||
| change_signature_expectation( | ||
| (f"{u.path}:{u.line}" for u in report.unmapped), | ||
| heuristic_allowed=self._allow_heuristic, | ||
| ), | ||
| delta, | ||
| rewritten=[ | ||
| (f"{s.path}:{s.line}", s.resolution) | ||
| for s in report.sites | ||
| if s.before != s.after | ||
| ], | ||
| ) | ||
| if verdict.ok: | ||
| return report._replace(verdict=verdict) | ||
| undo_last(self.repo_root) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '560,620p' codebase_rag/editing/transaction.py
rg -n 'undo_last' -A5 -B5 codebase_rag --glob '!*test*'
sed -n '1,140p' codebase_rag/editing/signature.pyRepository: vitali87/code-graph-rag Length of output: 12031 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- editing files ---'
git ls-files codebase_rag/editing
printf '%s\n' '--- rollback and transaction outcome references ---'
rg -n 'undo_transaction|undo_last|TransactionOutcome|applied=False|contract' codebase_rag/editing --glob '*.py' --glob '!*test*'
printf '%s\n' '--- transaction outcome implementation ---'
rg -n 'class TransactionOutcome|TransactionOutcome\\(|def commit|def _undo_newest' codebase_rag/editing/transaction.py -A35 -B8
printf '%s\n' '--- likely rename/edit operation outlines ---'
for f in $(git ls-files codebase_rag/editing | grep -E '(rename|edit|operation|refactor)\\.py$'); do
echo "### $f"
ast-grep outline "$f" 2>/dev/null | head -80 || true
doneRepository: vitali87/code-graph-rag Length of output: 5444 🤖 get_repo_knowledge executed:
Length of output: 956 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- transaction outcome and undo implementation ---'
sed -n '55,78p;340,410p;620,665p' codebase_rag/editing/transaction.py
printf '%s\n' '--- rename contract rollback ---'
sed -n '1040,1180p' codebase_rag/editing/rename.py
printf '%s\n' '--- rename imports and report transaction field context ---'
sed -n '35,60p;630,690p;760,810p' codebase_rag/editing/rename.pyRepository: vitali87/code-graph-rag Length of output: 17347 Roll back the signature transaction by ID.
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a failed contract check, |
||
| self.reingest(list(report.files)) | ||
| return report._replace( | ||
| applied=False, | ||
| verdict=verdict, | ||
| message=cs.SIGNATURE_CONTRACT_FAILED.format( | ||
| reasons="; ".join(verdict.failures) | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def change_signature( | ||
| repo_root: Path, | ||
| fetch_all: QueryFn, | ||
| project_name: str, | ||
| qualified_name: str, | ||
| new_params: Iterable[ParamSpec | str], | ||
| allow_heuristic: bool = False, | ||
| dry_run: bool = False, | ||
| verify: Callable[[StagedTree], VerificationResult | bool | None] | None = None, | ||
| reingest: Reingest | None = None, | ||
| ) -> SignatureReport: | ||
| """The op: plan (rewrites definitions and mapped sites) or plan and apply.""" | ||
| specs = [parse_param_spec(p) if isinstance(p, str) else p for p in new_params] | ||
| changer = SignatureChanger( | ||
| repo_root, fetch_all, project_name, verify=verify, reingest=reingest | ||
| ) | ||
| if dry_run: | ||
| report, _patcher = changer.plan(qualified_name, specs, allow_heuristic) | ||
| return report | ||
| return changer.apply(qualified_name, specs, allow_heuristic) | ||
|
Comment on lines
+101
to
+120
|
||
|
|
||
|
|
||
| __all__ = [ | ||
| "ParamSpec", | ||
| "RewrittenSite", | ||
| "SignatureChanger", | ||
| "SignatureRefused", | ||
| "SignatureReport", | ||
| "UnmappedSite", | ||
| "change_signature", | ||
| "parse_param_spec", | ||
| "sites_for", | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Omit
verdictfrom the initial_asdict()payload.report._asdict()already insertsverdict, so each conditional assignment still leaves"verdict": nullwhen no verdict exists.codebase_rag/cli.py#L1217-L1218: removecs.KEY_VERDICTbefore the condition inrename_command.codebase_rag/cli.py#L1277-L1278: removecs.KEY_VERDICTbefore the condition inchange_signature_command.codebase_rag/mcp/tools.py#L2588-L2589: removecs.KEY_VERDICTbefore the condition in_run_change_signature.🧰 Tools
🪛 ast-grep (0.45.3)
[info] 1218-1218: use jsonify instead of json.dumps for JSON output
Context: json.dumps(payload, indent=cs.MCP_JSON_INDENT, sort_keys=True)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
📍 Affects 2 files
codebase_rag/cli.py#L1217-L1218(this comment)codebase_rag/cli.py#L1277-L1278codebase_rag/mcp/tools.py#L2588-L2589🤖 Prompt for AI Agents