Analyze changes and produce risk-scored review guidance. Args: store: The graph store. changed_files: List of changed file paths. changed_ranges: Optional pre-parsed diff ranges. If not provided and ``repo_root`` is given, they are computed via the detected V
(
store: GraphStore,
changed_files: list[str],
changed_ranges: dict[str, list[tuple[int, int]]] | None = None,
repo_root: str | None = None,
base: str = "HEAD~1",
)
| 275 | |
| 276 | |
| 277 | def analyze_changes( |
| 278 | store: GraphStore, |
| 279 | changed_files: list[str], |
| 280 | changed_ranges: dict[str, list[tuple[int, int]]] | None = None, |
| 281 | repo_root: str | None = None, |
| 282 | base: str = "HEAD~1", |
| 283 | ) -> dict[str, Any]: |
| 284 | """Analyze changes and produce risk-scored review guidance. |
| 285 | |
| 286 | Args: |
| 287 | store: The graph store. |
| 288 | changed_files: List of changed file paths. |
| 289 | changed_ranges: Optional pre-parsed diff ranges. If not provided and |
| 290 | ``repo_root`` is given, they are computed via the detected VCS |
| 291 | (Git or SVN). |
| 292 | repo_root: Repository root (for git/svn diff). |
| 293 | base: Git ref or SVN revision range to diff against. |
| 294 | |
| 295 | Returns: |
| 296 | Dict with ``summary``, ``risk_score``, ``changed_functions``, |
| 297 | ``affected_flows``, ``test_gaps``, and ``review_priorities``. |
| 298 | """ |
| 299 | # Compute changed ranges if not provided. |
| 300 | if changed_ranges is None and repo_root is not None: |
| 301 | # Diff keys are forward-slash paths relative to the repo root, but |
| 302 | # the graph stores absolute native paths. Remap so lookups work on |
| 303 | # Windows, where the LIKE-suffix fallback cannot bridge |
| 304 | # "src/app.py" to "C:\repo\src\app.py" (#528). Keys that are |
| 305 | # already absolute pass through pathlib joining unchanged. The |
| 306 | # explicit changed_ranges path (MCP) is untouched — tools/review.py |
| 307 | # remaps before calling, and remapping twice would corrupt keys. |
| 308 | root_path = Path(repo_root) |
| 309 | changed_ranges = { |
| 310 | str(root_path / key): ranges |
| 311 | for key, ranges in parse_diff_ranges(repo_root, base).items() |
| 312 | } |
| 313 | |
| 314 | # Map changes to nodes. |
| 315 | if changed_ranges: |
| 316 | changed_nodes = map_changes_to_nodes(store, changed_ranges) |
| 317 | else: |
| 318 | # Fallback: all nodes in changed files. |
| 319 | changed_nodes = [] |
| 320 | for fp in changed_files: |
| 321 | changed_nodes.extend(store.get_nodes_by_file(fp)) |
| 322 | |
| 323 | # Filter to functions/tests for risk scoring (skip File nodes). |
| 324 | changed_funcs = [ |
| 325 | n for n in changed_nodes |
| 326 | if n.kind in ("Function", "Test", "Class") |
| 327 | ] |
| 328 | |
| 329 | # Cap to prevent O(N*M) query explosion on large PRs. |
| 330 | _max_funcs = int(os.environ.get("CRG_MAX_CHANGED_FUNCS", "500")) |
| 331 | funcs_truncated = len(changed_funcs) > _max_funcs |
| 332 | if funcs_truncated: |
| 333 | changed_funcs = changed_funcs[:_max_funcs] |
| 334 |