Skip to content

Commit 3288f59

Browse files
tirth8205claude
andauthored
feat: Elixir + refactor dry-run + CRG_DATA_DIR + cloud warning + docs (supersedes #204 #207 #179) (#228)
* docs(troubleshooting): add quick-reference for the 4 most common support patterns These four issues account for the majority of support questions: 1. "Hooks use a matcher + hooks array" — users on pre-v2.2.3 releases. Answer: upgrade to v2.2.4 and re-run `code-review-graph install`. 2. "code-review-graph: command not found" after pip install — PATH issue. Answer: use pipx, uvx, or `python -m code_review_graph`. 3. "Is this project-scoped or user-scoped?" — 4-piece scope table (package is user-scoped, graph+mcp config are project-scoped, multi-repo registry is user-scoped). 4. "Built the graph but Claude Code doesn't see it in a new session" — didn't restart Claude Code / wrong cwd / ran build but not install. The new section is placed at the top of TROUBLESHOOTING.md so it's found first by anyone searching for these error messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add Elixir parser support (#112) Closes #112. tree-sitter-elixir is already bundled with tree-sitter-language-pack so no new runtime dep. Elixir's AST is uniform: ``defmodule``, ``def``/``defp``/``defmacro``, ``alias``/``import``/``require``/``use``, and ordinary function invocations all share the ``call`` node type and are distinguished only by the leading identifier text. Added a dedicated ``_extract_elixir_constructs`` helper (following the R and Lua pattern) that dispatches by first-identifier: - ``defmodule Name do ... end`` -> Class node, recurse into do_block with ``enclosing_class=Name`` - ``def`` / ``defp`` / ``defmacro`` / ``defmacrop`` -> Function/Test node attached to the enclosing module, recurse into do_block with ``enclosing_func=<fn>`` so nested calls resolve to qualified names - ``alias`` / ``import`` / ``require`` / ``use`` -> IMPORTS_FROM edge targeting the module name (flat or dotted, e.g. ``Foo.Bar``) - Anything else -> CALLS edge with the leading identifier as the target name, followed by recursion into arguments + do_block for nested calls Helpers added: - ``_elixir_call_identifier`` — handles both plain identifiers and ``dot > alias + identifier`` for ``IO.puts`` style calls - ``_elixir_module_name`` — extracts a module name from an arguments subtree (``alias`` node or ``dot`` node) - ``_elixir_function_name_and_params`` — extracts the function name from the inner call node inside ``def``'s arguments Verified on ``tests/fixtures/sample.ex`` (two modules with 6 functions total across public/private def/defp, plus alias/import/require): - 9 nodes, 16 edges - 2 Class nodes (Calculator, MathHelpers) - 6 Function nodes with correct parent_name - 3 IMPORTS_FROM (alias, import, require) - 5 CALLS with internal resolution: Calculator.compute -> Calculator.add, MathHelpers.double -> Calculator.compute, MathHelpers.triple -> MathHelpers.double, etc. Tests (7 new in TestElixirParsing): - Language detection (.ex + .exs) - Module -> Class mapping - def/defp producing Function nodes with correct parents - alias/import/require producing IMPORTS_FROM - Internal call resolution to qualified names - CONTAINS edges linking module -> functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: apply_refactor_tool dry-run mode returning unified diffs (#176) Closes #176. The existing rename_preview / apply_refactor flow was technically two-step, but apply_refactor wrote to disk immediately so there was no way to inspect the exact diff before committing. apply_refactor() now takes an optional ``dry_run: bool`` parameter. When true: - Compute the same edit plan as the real write path, grouping edits by file so multiple edits to the same file apply sequentially against updated content (fixes a subtle bug where separate edits on the same file could stomp each other) - Return a unified-diff string per would-be-modified file using ``difflib.unified_diff`` - Leave the refactor_id in the pending cache so the user can review the diff then call again with ``dry_run=False`` to commit The MCP tool wrapper (``apply_refactor_tool`` in main.py + ``apply_refactor_func`` in tools/refactor_tools.py) now exposes the ``dry_run`` parameter. The real-write path was also refactored to share the plan-computation step with dry-run — multi-edit files now work correctly in both modes. Tests (2 new in test_refactor.py): - test_apply_refactor_dry_run_returns_diff_without_writing: confirms the diff contains both old and new names, the file on disk is unchanged, the refactor_id is still valid after the dry-run, and a follow-up real apply with the same id succeeds and consumes the id. - test_apply_refactor_dry_run_no_edits: empty edit list returns an empty diffs dict. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: CRG_DATA_DIR / CRG_REPO_ROOT env vars for out-of-tree graph storage (#155) Closes #155 (supersedes #207 which had unresolved review comments about input()-on-stdio and _local_only fragility). New env vars let users keep the .code-review-graph directory outside their working tree — useful for ephemeral workspaces, Docker volumes, shared CI caches, and multi-repo orchestrators where scripting the CLI from outside the repo is necessary. ## CRG_DATA_DIR When set, replaces the default ``<repo>/.code-review-graph`` directory verbatim. New ``get_data_dir(repo_root)`` helper in incremental.py is the single source of truth — it creates the directory (respecting the override) and writes the auto-generated inner ``.gitignore`` with ``*`` so graph files are never committed. Call sites updated to use ``get_data_dir()``: - ``incremental.py::get_db_path`` (the graph.db location) - ``cli.py::visualize`` (graph.html) - ``cli.py::wiki`` (wiki output dir) - ``tools/docs.py::generate_wiki_func`` (MCP wiki tool) - ``tools/docs.py::get_wiki_page_func`` (MCP wiki lookup) The legacy ``<repo>/.code-review-graph.db`` migration is preserved but only runs when CRG_DATA_DIR is unset (there's no relationship between the legacy location and an override location). ## CRG_REPO_ROOT ``find_project_root()`` now checks ``CRG_REPO_ROOT`` before the usual git-root walk. Useful for anyone scripting ``code-review-graph`` from a cwd that isn't inside the target repo (daemons, CI jobs, wrappers). Falls through to normal resolution if the path doesn't exist. ## Deliberate design choices (vs PR #207) - **No input() / confirmation prompts** — this branch of code runs from the CLI entry and can also be pulled into MCP wrappers which pipe stdio. Reading from stdin there would corrupt JSON-RPC. - **No ``mcp._local_only`` attribute** — that approach was fragile because it touched a private FastMCP attribute. - **Tilde expansion** — both env vars go through ``Path.expanduser()`` so ``CRG_DATA_DIR=~/Library/crg`` works. Tests (5 new in TestDataDir): - test_default_uses_repo_subdir — unset env, default behavior - test_env_override_replaces_repo_subdir — CRG_DATA_DIR works - test_get_db_path_uses_data_dir — graph.db follows the override - test_find_project_root_env_override — CRG_REPO_ROOT works - test_find_project_root_env_override_missing_dir_falls_through — bogus override doesn't crash, falls back to git root Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: stderr warning before cloud embedding providers exfiltrate code (#174) Closes #174 (supersedes #179 which overlapped with #207 and used input()-on-stdio that would corrupt MCP transport). ``get_provider()`` now prints an explicit warning to **stderr** (never stdout/stdin) before returning a Google Gemini or MiniMax provider: ⚠️ code-review-graph: about to embed code via the 'google' cloud provider. Your source code (function names, docstrings, file paths) will be sent to an external API. To skip this warning in future runs, set CRG_ACCEPT_CLOUD_EMBEDDINGS=1 in your environment. To stay fully offline, use the default 'local' provider instead (no API key needed). Why stderr: the MCP stdio transport uses stdout for JSON-RPC. Writing to stdout or reading from stdin would corrupt the transport. stderr is safe and is captured by Claude Code's MCP logs. Why a warning not a block: the user already explicitly chose a cloud provider (either via ``provider="google"`` arg or via the MiniMax API key + explicit provider selection). Blocking would be paternalistic; warning is informative. The ``CRG_ACCEPT_CLOUD_EMBEDDINGS=1`` env var lets CI / scripted workflows acknowledge once and move on without repeated noise. Tests (4 new in TestCloudProviderWarning): - test_minimax_triggers_stderr_warning — captures stderr, asserts warning content, asserts stdout stays clean - test_google_triggers_stderr_warning — same for google - test_accept_env_var_suppresses_warning — CRG_ACCEPT_CLOUD_EMBEDDINGS=1 silences the warning - test_local_provider_never_warns — offline path stays quiet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8c1a7fb commit 3288f59

14 files changed

Lines changed: 979 additions & 63 deletions

code_review_graph/cli.py

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,61 @@ def _print_banner() -> None:
9595
""")
9696

9797

98+
def _instruction_files_to_modify(
99+
repo_root: Path, target: str,
100+
) -> list[str]:
101+
"""Return the list of instruction files that ``install`` would write
102+
or modify, given the current state of the repo and the selected
103+
platform target. Used for the dry-run / confirm preview (#173).
104+
"""
105+
from .skills import _CLAUDE_MD_SECTION_MARKER, _PLATFORM_INSTRUCTION_FILES
106+
107+
targets: list[str] = []
108+
109+
if target in ("claude", "all"):
110+
claude_md = repo_root / "CLAUDE.md"
111+
if claude_md.exists():
112+
content = claude_md.read_text(encoding="utf-8")
113+
if _CLAUDE_MD_SECTION_MARKER not in content:
114+
targets.append("CLAUDE.md (append)")
115+
else:
116+
targets.append("CLAUDE.md (new)")
117+
118+
for filename, owners in _PLATFORM_INSTRUCTION_FILES.items():
119+
if target != "all" and target not in owners:
120+
continue
121+
path = repo_root / filename
122+
if path.exists():
123+
content = path.read_text(encoding="utf-8")
124+
if _CLAUDE_MD_SECTION_MARKER not in content:
125+
targets.append(f"{filename} (append)")
126+
else:
127+
targets.append(f"{filename} (new)")
128+
129+
return targets
130+
131+
132+
def _confirm_yes_no(prompt: str, default_yes: bool = True) -> bool:
133+
"""Prompt the user [Y/n] and return True for yes.
134+
135+
Non-interactive environments (no TTY on stdin, e.g. an MCP wrapper
136+
piping the CLI) return ``default_yes`` without blocking — the
137+
stdio transport cannot safely read from stdin without corrupting
138+
the JSON-RPC stream. See: #173, #174
139+
"""
140+
if not sys.stdin.isatty():
141+
return default_yes
142+
suffix = "[Y/n]" if default_yes else "[y/N]"
143+
try:
144+
answer = input(f"{prompt} {suffix} ").strip().lower()
145+
except (EOFError, KeyboardInterrupt):
146+
print()
147+
return False
148+
if not answer:
149+
return default_yes
150+
return answer in ("y", "yes")
151+
152+
98153
def _handle_init(args: argparse.Namespace) -> None:
99154
"""Set up MCP config for detected AI coding platforms."""
100155
from .incremental import ensure_repo_gitignore_excludes_crg, find_repo_root
@@ -108,6 +163,8 @@ def _handle_init(args: argparse.Namespace) -> None:
108163
target = getattr(args, "platform", "all") or "all"
109164
if target == "claude-code":
110165
target = "claude"
166+
auto_yes = getattr(args, "yes", False)
167+
skip_instructions = getattr(args, "no_instructions", False)
111168

112169
print("Installing MCP server config...")
113170
configured = install_platform_configs(repo_root, target=target, dry_run=dry_run)
@@ -117,9 +174,17 @@ def _handle_init(args: argparse.Namespace) -> None:
117174
else:
118175
print(f"\nConfigured {len(configured)} platform(s): {', '.join(configured)}")
119176

177+
# Preview the instruction files that would be touched (#173).
178+
instr_targets = _instruction_files_to_modify(repo_root, target)
179+
if instr_targets:
180+
print()
181+
print("Graph instructions will be injected into:")
182+
for t in instr_targets:
183+
print(f" {t}")
184+
120185
if dry_run:
121-
print("[dry-run] Would ensure .gitignore ignores .code-review-graph/.")
122-
print("\n[dry-run] No files were modified.")
186+
print("\n[dry-run] Would ensure .gitignore ignores .code-review-graph/.")
187+
print("[dry-run] No files were modified.")
123188
return
124189

125190
gitignore_state = ensure_repo_gitignore_excludes_crg(repo_root)
@@ -131,7 +196,8 @@ def _handle_init(args: argparse.Namespace) -> None:
131196
print(".gitignore already contains .code-review-graph/.")
132197

133198
# Skills and hooks are installed by default so Claude actually uses the
134-
# graph tools proactively. Use --no-skills / --no-hooks to opt out.
199+
# graph tools proactively. Use --no-skills / --no-hooks / --no-instructions
200+
# to opt out.
135201
skip_skills = getattr(args, "no_skills", False)
136202
skip_hooks = getattr(args, "no_hooks", False)
137203
# Legacy: --skills/--hooks/--all still accepted (no-op, everything is default)
@@ -147,11 +213,26 @@ def _handle_init(args: argparse.Namespace) -> None:
147213
if not skip_skills:
148214
skills_dir = generate_skills(repo_root)
149215
print(f"Generated skills in {skills_dir}")
150-
if target in ("claude", "all"):
151-
inject_claude_md(repo_root)
152-
updated = inject_platform_instructions(repo_root, target=target)
153-
if updated:
154-
print(f"Injected graph instructions into: {', '.join(updated)}")
216+
217+
# Confirm before writing instruction files (#173). --yes skips the
218+
# prompt; --no-instructions skips the whole block.
219+
if not skip_instructions and instr_targets:
220+
if auto_yes or _confirm_yes_no(
221+
"Inject graph instructions into the files above?",
222+
default_yes=True,
223+
):
224+
if target in ("claude", "all"):
225+
inject_claude_md(repo_root)
226+
inject_platform_instructions(repo_root, target=target)
227+
# Use the precomputed instr_targets list for the confirmation
228+
# message; we don't need the fresh return value from
229+
# inject_platform_instructions here.
230+
names = [t.split(" ")[0] for t in instr_targets]
231+
print(f"Injected graph instructions into: {', '.join(names)}")
232+
else:
233+
print("Skipped instruction injection (user declined).")
234+
elif skip_instructions:
235+
print("Skipped instruction injection (--no-instructions).")
155236

156237
if not skip_hooks and target in ("claude", "all"):
157238
install_hooks(repo_root)
@@ -194,6 +275,14 @@ def main() -> None:
194275
"--no-hooks", action="store_true",
195276
help="Skip installing Claude Code hooks",
196277
)
278+
install_cmd.add_argument(
279+
"--no-instructions", action="store_true",
280+
help="Skip injecting graph instructions into CLAUDE.md / AGENTS.md / etc.",
281+
)
282+
install_cmd.add_argument(
283+
"-y", "--yes", action="store_true",
284+
help="Auto-confirm instruction injection without an interactive prompt",
285+
)
197286
# Legacy flags (kept for backwards compat, now no-ops since all is default)
198287
install_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS)
199288
install_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS)
@@ -225,6 +314,14 @@ def main() -> None:
225314
"--no-hooks", action="store_true",
226315
help="Skip installing Claude Code hooks",
227316
)
317+
init_cmd.add_argument(
318+
"--no-instructions", action="store_true",
319+
help="Skip injecting graph instructions into CLAUDE.md / AGENTS.md / etc.",
320+
)
321+
init_cmd.add_argument(
322+
"-y", "--yes", action="store_true",
323+
help="Auto-confirm instruction injection without an interactive prompt",
324+
)
228325
init_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS)
229326
init_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS)
230327
init_cmd.add_argument("--all", action="store_true", dest="install_all",
@@ -553,8 +650,9 @@ def main() -> None:
553650
watch(repo_root, store)
554651

555652
elif args.command == "visualize":
653+
from .incremental import get_data_dir
556654
from .visualization import generate_html
557-
html_path = repo_root / ".code-review-graph" / "graph.html"
655+
html_path = get_data_dir(repo_root) / "graph.html"
558656
vis_mode = getattr(args, "mode", "auto") or "auto"
559657
generate_html(store, html_path, mode=vis_mode)
560658
print(f"Visualization ({vis_mode}): {html_path}")
@@ -579,8 +677,9 @@ def main() -> None:
579677
print("Open in browser to explore your codebase graph.")
580678

581679
elif args.command == "wiki":
680+
from .incremental import get_data_dir
582681
from .wiki import generate_wiki
583-
wiki_dir = repo_root / ".code-review-graph" / "wiki"
682+
wiki_dir = get_data_dir(repo_root) / "wiki"
584683
result = generate_wiki(store, wiki_dir, force=args.force)
585684
total = result["pages_generated"] + result["pages_updated"] + result["pages_unchanged"]
586685
print(

code_review_graph/embeddings.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import os
1414
import sqlite3
1515
import struct
16+
import sys
1617
import time
1718
from abc import ABC, abstractmethod
1819
from pathlib import Path
@@ -246,6 +247,35 @@ def name(self) -> str:
246247
return f"minimax:{self._MODEL}"
247248

248249

250+
CLOUD_PROVIDERS = {"google", "minimax"}
251+
252+
253+
def _warn_cloud_egress(provider_name: str) -> None:
254+
"""Print a stderr warning before a cloud embedding provider is used.
255+
256+
The warning is suppressed when ``CRG_ACCEPT_CLOUD_EMBEDDINGS=1`` is
257+
set in the environment, so scripted / CI workloads can acknowledge
258+
once and move on. Use stderr (never stdin/input) to stay compatible
259+
with the MCP stdio transport — anything we write to stdout would
260+
corrupt the JSON-RPC stream. See: #174
261+
"""
262+
if os.environ.get("CRG_ACCEPT_CLOUD_EMBEDDINGS", "").strip() == "1":
263+
return
264+
print(
265+
f"\n⚠️ code-review-graph: about to embed code via the '{provider_name}' "
266+
"cloud provider.\n"
267+
" Your source code (function names, docstrings, file paths) will be "
268+
"sent to an external API.\n"
269+
" This is necessary for semantic search with the cloud provider you "
270+
"selected.\n"
271+
" To skip this warning in future runs, set "
272+
"CRG_ACCEPT_CLOUD_EMBEDDINGS=1 in your environment.\n"
273+
" To stay fully offline, use the default 'local' provider instead "
274+
"(no API key needed).\n",
275+
file=sys.stderr,
276+
)
277+
278+
249279
def get_provider(
250280
provider: str | None = None,
251281
model: str | None = None,
@@ -256,6 +286,8 @@ def get_provider(
256286
provider: Provider name. One of "local", "google", "minimax", or None for local.
257287
Google requires GOOGLE_API_KEY env var and explicit opt-in.
258288
MiniMax requires MINIMAX_API_KEY env var and explicit opt-in.
289+
Cloud providers emit a one-time stderr warning before use
290+
unless ``CRG_ACCEPT_CLOUD_EMBEDDINGS=1`` is set. See: #174
259291
model: Model name/path to use. For local provider this is any
260292
sentence-transformers compatible model. Falls back to
261293
CRG_EMBEDDING_MODEL env var, then to all-MiniLM-L6-v2.
@@ -268,6 +300,7 @@ def get_provider(
268300
"MINIMAX_API_KEY environment variable is required for "
269301
"the MiniMax embedding provider."
270302
)
303+
_warn_cloud_egress("minimax")
271304
return MiniMaxEmbeddingProvider(api_key=api_key)
272305

273306
if provider == "google":
@@ -277,6 +310,7 @@ def get_provider(
277310
"GOOGLE_API_KEY environment variable is required for "
278311
"the Google embedding provider."
279312
)
313+
_warn_cloud_egress("google")
280314
try:
281315
return GoogleEmbeddingProvider(
282316
api_key=api_key,

code_review_graph/incremental.py

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -84,37 +84,76 @@ def find_repo_root(start: Path | None = None) -> Optional[Path]:
8484

8585

8686
def find_project_root(start: Path | None = None) -> Path:
87-
"""Find the project root: git repo root if available, otherwise cwd."""
87+
"""Find the project root.
88+
89+
Resolution order (highest precedence first):
90+
91+
1. ``CRG_REPO_ROOT`` environment variable — explicit override for
92+
anyone scripting the CLI from outside the repo (CI jobs, daemons,
93+
multi-repo orchestrators). See: #155
94+
2. Git repository root via :func:`find_repo_root` from ``start``.
95+
3. ``start`` itself (or cwd if no start given).
96+
"""
97+
env_override = os.environ.get("CRG_REPO_ROOT", "").strip()
98+
if env_override:
99+
p = Path(env_override).expanduser().resolve()
100+
if p.exists():
101+
return p
88102
root = find_repo_root(start)
89103
if root:
90104
return root
91105
return start or Path.cwd()
92106

93107

94-
def get_db_path(repo_root: Path) -> Path:
95-
"""Determine the database path for a repository.
108+
def get_data_dir(repo_root: Path) -> Path:
109+
"""Return the directory where this project's graph data lives.
96110
97-
Creates the ``.code-review-graph/`` directory and an inner ``.gitignore``
98-
(with ``*``) so generated files are never committed. If a legacy
99-
``.code-review-graph.db`` exists at the repo root the database is migrated
100-
into the new directory (WAL/SHM side-files are discarded).
111+
By default, ``<repo_root>/.code-review-graph``. If the
112+
``CRG_DATA_DIR`` environment variable is set, it is used verbatim
113+
instead — letting you keep graphs outside the working tree (useful
114+
for ephemeral workspaces, Docker volumes, or shared caches). See: #155
115+
116+
The directory is created if it does not already exist; an inner
117+
``.gitignore`` (with ``*``) is written so any accidentally-nested
118+
files never get committed. Both are idempotent.
101119
"""
102-
crg_dir = repo_root / ".code-review-graph"
103-
new_db = crg_dir / "graph.db"
120+
env_override = os.environ.get("CRG_DATA_DIR", "").strip()
121+
if env_override:
122+
data_dir = Path(env_override).expanduser().resolve()
123+
else:
124+
data_dir = repo_root / ".code-review-graph"
104125

105-
# Ensure directory exists
106-
crg_dir.mkdir(exist_ok=True)
126+
data_dir.mkdir(parents=True, exist_ok=True)
107127

108-
# Auto-create .gitignore inside the directory (idempotent)
109-
inner_gitignore = crg_dir / ".gitignore"
128+
inner_gitignore = data_dir / ".gitignore"
110129
if not inner_gitignore.exists():
111-
inner_gitignore.write_text(
112-
"# Auto-generated by code-review-graph — do not commit database files.\n"
113-
"# The graph.db contains absolute paths and code structure metadata.\n"
114-
"*\n"
115-
)
130+
try:
131+
inner_gitignore.write_text(
132+
"# Auto-generated by code-review-graph — do not commit database files.\n"
133+
"# The graph.db contains absolute paths and code structure metadata.\n"
134+
"*\n"
135+
)
136+
except OSError:
137+
# Data dir might be read-only (rare); that's OK, it's a best-effort guard.
138+
pass
139+
140+
return data_dir
141+
142+
143+
def get_db_path(repo_root: Path) -> Path:
144+
"""Determine the database path for a repository.
145+
146+
Respects ``CRG_DATA_DIR`` (see :func:`get_data_dir`). Migrates a
147+
legacy top-level ``.code-review-graph.db`` file into the new
148+
directory when it exists (WAL/SHM side-files are discarded).
149+
"""
150+
crg_dir = get_data_dir(repo_root)
151+
new_db = crg_dir / "graph.db"
116152

117-
# Migrate legacy database if present
153+
# Migrate legacy database if present (only meaningful when the
154+
# legacy file sits at the repo root — if CRG_DATA_DIR is set we
155+
# skip the migration because there's no relationship between the
156+
# legacy location and the new one).
118157
legacy_db = repo_root / ".code-review-graph.db"
119158
if legacy_db.exists() and not new_db.exists():
120159
legacy_db.rename(new_db)

code_review_graph/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ def refactor_tool(
571571
def apply_refactor_tool(
572572
refactor_id: str,
573573
repo_root: Optional[str] = None,
574+
dry_run: bool = False,
574575
) -> dict:
575576
"""Apply a previously previewed refactoring to source files.
576577
@@ -584,9 +585,15 @@ def apply_refactor_tool(
584585
Args:
585586
refactor_id: The refactor ID from refactor_tool's response.
586587
repo_root: Repository root path. Auto-detected if omitted.
588+
dry_run: If True, return a unified diff of what would change
589+
without touching any files. The refactor_id remains valid so
590+
the same preview can be applied in a follow-up call without
591+
dry_run. Use this for a human-in-the-loop review before
592+
committing changes to disk. See: #176
587593
"""
588594
return apply_refactor_func(
589595
refactor_id=refactor_id, repo_root=_resolve_repo_root(repo_root),
596+
dry_run=dry_run,
590597
)
591598

592599

0 commit comments

Comments
 (0)