Skip to content

Commit 6b79d9d

Browse files
QingtaoLi1Copilot
andauthored
Fix/update rpg graph stats (#83)
## Summary - Fix `run_update_rpg()` statistics so `edge_count` / `edges_delta` are computed from the serialized RPG JSON that is actually written to disk, rather than from `updated_rpg.edges`. - Add dependency graph stats to update/check outputs: - `dep_nodes` - `dep_edges` - `dep_nodes_delta` - `dep_edges_delta` - `dep_to_rpg_map_size` - Update `/cmind.update_rpg` instructions to display feature graph and dependency graph counts separately. - Extend tests to cover serialized RPG edge counts and embedded dependency graph stats. ## Why `RPG.to_dict()` merges dep-graph-derived semantic edges into the serialized `rpg.json`. The previous `update_rpg` result used `len(updated_rpg.edges)`, so it could report `edges_delta: 0` even when the persisted RPG edge set changed substantially. This makes the command output match the graph data users actually inspect via `rpg.json` and `check_encode.py`. ## Test plan - [x] Targeted pytest: ```bash uv run --with . --with pytest --with pytest-timeout python -m pytest -q tests/test_encode_commands.py tests/test_step4_integration.py -k "check_encode or run_update_rpg or update_rpg_template" --timeout=60 Result: 5 passed, 39 deselected - Verified local check_encode output includes dependency graph stats: { "node_count": 7425, "edge_count": 37004, "dep_nodes": 2661, "dep_edges": 5394 } - RPG edit impact review passed. Notes The full default smoke test still fails on existing unrelated repository-wide issues: - import path failures for top-level common / rpg imports during full-repo smoke - 7 unrelated stub detections Those failures are not introduced by this PR; the scoped scripts import smoke passes. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 1d4da22 commit 6b79d9d

6 files changed

Lines changed: 134 additions & 23 deletions

File tree

CoderMind/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cmind-cli"
3-
version = "0.1.3"
3+
version = "0.1.9"
44
description = "CoderMind CLI - A tool to generate feature trees for repository planning and code generation."
55
requires-python = ">=3.12"
66
dependencies = [

CoderMind/scripts/rpg_encoder/check_encode.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,36 @@ def load_json(path: Path) -> Dict[str, Any] | None:
6464
return None
6565

6666

67+
def _count_graph_items(value: Any) -> int:
68+
return len(value) if isinstance(value, (list, dict)) else 0
69+
70+
71+
def _embedded_dep_graph(data: Dict[str, Any]) -> Dict[str, Any]:
72+
dep_graph = data.get("dep_graph")
73+
if isinstance(dep_graph, dict):
74+
return dep_graph
75+
76+
rpg_data = data.get("rpg")
77+
if isinstance(rpg_data, dict) and isinstance(rpg_data.get("structure"), dict):
78+
dep_graph = rpg_data["structure"].get("dep_graph")
79+
if isinstance(dep_graph, dict):
80+
return dep_graph
81+
82+
return {}
83+
84+
6785
def get_rpg_stats(data: Dict[str, Any]) -> Dict[str, Any]:
6886
"""Extract basic statistics from RPG JSON data."""
6987
stats: Dict[str, Any] = {}
7088
stats["repo_name"] = data.get("repo_name", "unknown")
7189

7290
# Count nodes
7391
nodes = data.get("nodes", [])
74-
if isinstance(nodes, list):
75-
stats["node_count"] = len(nodes)
76-
elif isinstance(nodes, dict):
77-
stats["node_count"] = len(nodes)
78-
else:
79-
stats["node_count"] = 0
92+
stats["node_count"] = _count_graph_items(nodes)
8093

8194
# Count edges
8295
edges = data.get("edges", [])
83-
if isinstance(edges, list):
84-
stats["edge_count"] = len(edges)
85-
elif isinstance(edges, dict):
86-
stats["edge_count"] = len(edges)
87-
else:
88-
stats["edge_count"] = 0
96+
stats["edge_count"] = _count_graph_items(edges)
8997

9098
# Check for nested rpg.structure format
9199
rpg_data = data.get("rpg", {})
@@ -94,8 +102,12 @@ def get_rpg_stats(data: Dict[str, Any]) -> Dict[str, Any]:
94102
if isinstance(structure, dict):
95103
nodes_s = structure.get("nodes", [])
96104
edges_s = structure.get("edges", [])
97-
stats["node_count"] = len(nodes_s) if isinstance(nodes_s, (list, dict)) else 0
98-
stats["edge_count"] = len(edges_s) if isinstance(edges_s, (list, dict)) else 0
105+
stats["node_count"] = _count_graph_items(nodes_s)
106+
stats["edge_count"] = _count_graph_items(edges_s)
107+
108+
dep_graph = _embedded_dep_graph(data)
109+
stats["dep_nodes"] = _count_graph_items(dep_graph.get("nodes", []))
110+
stats["dep_edges"] = _count_graph_items(dep_graph.get("edges", []))
99111

100112
# Check for tree format with 'root' key (nested children structure)
101113
root = data.get("root")

CoderMind/scripts/rpg_encoder/run_update_rpg.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import sys
1818
import argparse
1919
from pathlib import Path
20+
from typing import Any
2021

2122
logger = logging.getLogger(__name__)
2223

@@ -33,6 +34,72 @@
3334
from common.rpg_io import atomic_write_rpg # noqa: E402
3435

3536

37+
def _count_serialized_items(value: Any) -> int:
38+
return len(value) if isinstance(value, (list, dict)) else 0
39+
40+
41+
def _serialized_feature_payload(data: dict[str, Any]) -> dict[str, Any]:
42+
rpg_data = data.get("rpg")
43+
if isinstance(rpg_data, dict) and isinstance(rpg_data.get("structure"), dict):
44+
return rpg_data["structure"]
45+
return data
46+
47+
48+
def _serialized_feature_edges(data: dict[str, Any]) -> int:
49+
edges = _serialized_feature_payload(data).get("edges", [])
50+
if isinstance(edges, list):
51+
# Flat-format RPGs may include hierarchy edges; exclude them so counts
52+
# match the edges actually persisted by RPG.to_dict().
53+
return sum(
54+
1
55+
for e in edges
56+
if not (
57+
isinstance(e, dict)
58+
and str(e.get("relation", "")).lower()
59+
in ("contains", "composes", "contains_base_class")
60+
)
61+
)
62+
return _count_serialized_items(edges)
63+
64+
65+
def _serialized_dep_graph(data: dict[str, Any]) -> dict[str, Any]:
66+
dep_graph = data.get("dep_graph")
67+
if isinstance(dep_graph, dict):
68+
return dep_graph
69+
payload = _serialized_feature_payload(data)
70+
dep_graph = payload.get("dep_graph")
71+
return dep_graph if isinstance(dep_graph, dict) else {}
72+
73+
74+
def _serialized_dep_stats(data: dict[str, Any]) -> dict[str, int]:
75+
dep_graph = _serialized_dep_graph(data)
76+
return {
77+
"nodes": _count_serialized_items(dep_graph.get("nodes", [])),
78+
"edges": _count_serialized_items(dep_graph.get("edges", [])),
79+
}
80+
81+
82+
def _serialized_dep_to_rpg_map_size(data: dict[str, Any]) -> int:
83+
dep_to_rpg_map = data.get("_dep_to_rpg_map")
84+
if isinstance(dep_to_rpg_map, dict):
85+
return len(dep_to_rpg_map)
86+
87+
nodes = _serialized_dep_graph(data).get("nodes", {})
88+
if isinstance(nodes, dict):
89+
return sum(
90+
1
91+
for attrs in nodes.values()
92+
if isinstance(attrs, dict) and attrs.get("rpg_nodes")
93+
)
94+
if isinstance(nodes, list):
95+
return sum(
96+
1
97+
for attrs in nodes
98+
if isinstance(attrs, dict) and attrs.get("rpg_nodes")
99+
)
100+
return 0
101+
102+
36103
def run_update_rpg(
37104
rpg_file: str,
38105
last_repo_dir: str,
@@ -111,7 +178,8 @@ def run_update_rpg(
111178
# Record pre-update stats + previous git meta so we can report
112179
# how the sync baseline advanced.
113180
pre_nodes = len(rpg.nodes)
114-
pre_edges = len(rpg.edges)
181+
pre_edges = _serialized_feature_edges(data)
182+
pre_dep_stats = _serialized_dep_stats(data)
115183
pre_commit = (rpg.git_meta or {}).get("head_commit")
116184

117185
# === Step 1: LLM-driven feature graph refactor ===
@@ -184,7 +252,8 @@ def run_update_rpg(
184252

185253
# Collect stats
186254
post_nodes = len(updated_rpg.nodes)
187-
post_edges = len(updated_rpg.edges)
255+
post_edges = _serialized_feature_edges(result_data)
256+
post_dep_stats = _serialized_dep_stats(result_data)
188257

189258
stats = {
190259
"repo_name": repo_name,
@@ -194,6 +263,11 @@ def run_update_rpg(
194263
"edge_count": post_edges,
195264
"nodes_delta": post_nodes - pre_nodes,
196265
"edges_delta": post_edges - pre_edges,
266+
"dep_nodes": post_dep_stats["nodes"],
267+
"dep_edges": post_dep_stats["edges"],
268+
"dep_nodes_delta": post_dep_stats["nodes"] - pre_dep_stats["nodes"],
269+
"dep_edges_delta": post_dep_stats["edges"] - pre_dep_stats["edges"],
270+
"dep_to_rpg_map_size": _serialized_dep_to_rpg_map_size(result_data),
197271
"aligned": enrich_stats.get("aligned", 0),
198272
"groups_pathed": enrich_stats.get("groups_pathed", 0),
199273
"l1_pathed": enrich_stats.get("l1_pathed", 0),

CoderMind/templates/commands/update_rpg.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ Inspect the `type` field in the JSON output:
4545
corrupt; the user may need to delete it and rerun `/cmind.encode`.
4646
* **`init`** → no `rpg.json` yet. Tell the user to run `/cmind.encode`
4747
first to create the baseline graph, then terminate.
48-
* **`update`** → display `result.stats.repo_name` / `node_count` /
49-
`edge_count` and proceed to Step 2.
48+
* **`update`** → display `result.stats.repo_name`, Feature graph
49+
`node_count` / `edge_count`, and Dependency graph `dep_nodes` /
50+
`dep_edges`, then proceed to Step 2.
5051

5152
Also verify there is at least one previous commit (the update needs
5253
`HEAD~1` as baseline):
@@ -80,8 +81,10 @@ not need to redirect output.
8081
RPG update complete!
8182
Repository: <repo_name>
8283
Previous ref: <prev_ref>
83-
Nodes: <node_count> (delta: <nodes_delta>)
84-
Edges: <edge_count> (delta: <edges_delta>)
84+
Feature graph Nodes: <node_count> (delta: <nodes_delta>)
85+
Feature graph Edges: <edge_count> (delta: <edges_delta>)
86+
Dependency graph Nodes: <dep_nodes> (delta: <dep_nodes_delta>)
87+
Dependency graph Edges: <dep_edges> (delta: <dep_edges_delta>)
8588
Aligned to dep_graph: <aligned>
8689
Functional areas: <functional_areas>
8790
Saved to: <output_path>

CoderMind/tests/test_encode_commands.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,17 @@ def test_update_state_valid_rpg(self, tmp_path, monkeypatch):
111111
cmind_data = tmp_path / ".cmind" / "data"
112112
cmind_data.mkdir(parents=True)
113113
rpg_file = cmind_data / "rpg.json"
114-
rpg_file.write_text(json.dumps(_make_rpg_data(), indent=2))
114+
rpg_data = _make_rpg_data()
115+
rpg_data["dep_graph"] = {
116+
"nodes": {
117+
"main.py": {"type": "file"},
118+
"main.py:hello": {"type": "function"},
119+
},
120+
"edges": [
121+
{"src": "main.py", "dst": "main.py:hello", "attrs": {"type": "contains"}},
122+
],
123+
}
124+
rpg_file.write_text(json.dumps(rpg_data, indent=2))
115125

116126
from rpg_encoder.check_encode import check_encode
117127
result = check_encode()
@@ -120,6 +130,8 @@ def test_update_state_valid_rpg(self, tmp_path, monkeypatch):
120130
assert result["stats"]["repo_name"] == "test_repo"
121131
assert result["stats"]["node_count"] == 2
122132
assert result["stats"]["edge_count"] == 1
133+
assert result["stats"]["dep_nodes"] == 2
134+
assert result["stats"]["dep_edges"] == 1
123135

124136
def test_error_state_invalid_rpg(self, tmp_path, monkeypatch):
125137
"""When rpg.json exists but has invalid format, return type=error."""
@@ -348,6 +360,8 @@ def test_update_rpg_template_references_check_script(self):
348360
content = f.read()
349361
assert "check_encode.py" in content
350362
assert "update_graphs.py update-rpg" in content
363+
assert "Dependency graph Nodes: <dep_nodes> (delta: <dep_nodes_delta>)" in content
364+
assert "Dependency graph Edges: <dep_edges> (delta: <dep_edges_delta>)" in content
351365

352366

353367
# ============================================================================

CoderMind/tests/test_step4_integration.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,15 @@ def test_run_update_rpg_advances_meta_git_and_runs_align(update_rpg_workspace, m
388388

389389
# dep_graph is embedded in rpg.json (single source of truth)
390390
assert "dep_graph" in persisted
391-
assert persisted["dep_graph"]["nodes"]
391+
dep_graph = persisted["dep_graph"]
392+
assert dep_graph["nodes"]
393+
assert result["edge_count"] == len(persisted["edges"])
394+
assert result["nodes_delta"] == 0
395+
assert result["edges_delta"] == 0
396+
assert result["dep_nodes"] == len(dep_graph["nodes"])
397+
assert result["dep_edges"] == len(dep_graph["edges"])
398+
assert result["dep_nodes_delta"] == 0
399+
assert result["dep_edges_delta"] == 0
392400

393401

394402
def test_run_update_rpg_dep_graph_path_default_matches_constant(monkeypatch, tmp_path):

0 commit comments

Comments
 (0)