diff --git a/CoderMind/pyproject.toml b/CoderMind/pyproject.toml index 2b3e7cf..f73a08c 100644 --- a/CoderMind/pyproject.toml +++ b/CoderMind/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cmind-cli" -version = "0.1.3" +version = "0.1.9" description = "CoderMind CLI - A tool to generate feature trees for repository planning and code generation." requires-python = ">=3.12" dependencies = [ diff --git a/CoderMind/scripts/rpg_encoder/check_encode.py b/CoderMind/scripts/rpg_encoder/check_encode.py index b4d2033..5925857 100644 --- a/CoderMind/scripts/rpg_encoder/check_encode.py +++ b/CoderMind/scripts/rpg_encoder/check_encode.py @@ -64,6 +64,24 @@ def load_json(path: Path) -> Dict[str, Any] | None: return None +def _count_graph_items(value: Any) -> int: + return len(value) if isinstance(value, (list, dict)) else 0 + + +def _embedded_dep_graph(data: Dict[str, Any]) -> Dict[str, Any]: + dep_graph = data.get("dep_graph") + if isinstance(dep_graph, dict): + return dep_graph + + rpg_data = data.get("rpg") + if isinstance(rpg_data, dict) and isinstance(rpg_data.get("structure"), dict): + dep_graph = rpg_data["structure"].get("dep_graph") + if isinstance(dep_graph, dict): + return dep_graph + + return {} + + def get_rpg_stats(data: Dict[str, Any]) -> Dict[str, Any]: """Extract basic statistics from RPG JSON data.""" stats: Dict[str, Any] = {} @@ -71,21 +89,11 @@ def get_rpg_stats(data: Dict[str, Any]) -> Dict[str, Any]: # Count nodes nodes = data.get("nodes", []) - if isinstance(nodes, list): - stats["node_count"] = len(nodes) - elif isinstance(nodes, dict): - stats["node_count"] = len(nodes) - else: - stats["node_count"] = 0 + stats["node_count"] = _count_graph_items(nodes) # Count edges edges = data.get("edges", []) - if isinstance(edges, list): - stats["edge_count"] = len(edges) - elif isinstance(edges, dict): - stats["edge_count"] = len(edges) - else: - stats["edge_count"] = 0 + stats["edge_count"] = _count_graph_items(edges) # Check for nested rpg.structure format rpg_data = data.get("rpg", {}) @@ -94,8 +102,12 @@ def get_rpg_stats(data: Dict[str, Any]) -> Dict[str, Any]: if isinstance(structure, dict): nodes_s = structure.get("nodes", []) edges_s = structure.get("edges", []) - stats["node_count"] = len(nodes_s) if isinstance(nodes_s, (list, dict)) else 0 - stats["edge_count"] = len(edges_s) if isinstance(edges_s, (list, dict)) else 0 + stats["node_count"] = _count_graph_items(nodes_s) + stats["edge_count"] = _count_graph_items(edges_s) + + dep_graph = _embedded_dep_graph(data) + stats["dep_nodes"] = _count_graph_items(dep_graph.get("nodes", [])) + stats["dep_edges"] = _count_graph_items(dep_graph.get("edges", [])) # Check for tree format with 'root' key (nested children structure) root = data.get("root") diff --git a/CoderMind/scripts/rpg_encoder/run_update_rpg.py b/CoderMind/scripts/rpg_encoder/run_update_rpg.py index 440d2c5..c368ef4 100644 --- a/CoderMind/scripts/rpg_encoder/run_update_rpg.py +++ b/CoderMind/scripts/rpg_encoder/run_update_rpg.py @@ -17,6 +17,7 @@ import sys import argparse from pathlib import Path +from typing import Any logger = logging.getLogger(__name__) @@ -33,6 +34,72 @@ from common.rpg_io import atomic_write_rpg # noqa: E402 +def _count_serialized_items(value: Any) -> int: + return len(value) if isinstance(value, (list, dict)) else 0 + + +def _serialized_feature_payload(data: dict[str, Any]) -> dict[str, Any]: + rpg_data = data.get("rpg") + if isinstance(rpg_data, dict) and isinstance(rpg_data.get("structure"), dict): + return rpg_data["structure"] + return data + + +def _serialized_feature_edges(data: dict[str, Any]) -> int: + edges = _serialized_feature_payload(data).get("edges", []) + if isinstance(edges, list): + # Flat-format RPGs may include hierarchy edges; exclude them so counts + # match the edges actually persisted by RPG.to_dict(). + return sum( + 1 + for e in edges + if not ( + isinstance(e, dict) + and str(e.get("relation", "")).lower() + in ("contains", "composes", "contains_base_class") + ) + ) + return _count_serialized_items(edges) + + +def _serialized_dep_graph(data: dict[str, Any]) -> dict[str, Any]: + dep_graph = data.get("dep_graph") + if isinstance(dep_graph, dict): + return dep_graph + payload = _serialized_feature_payload(data) + dep_graph = payload.get("dep_graph") + return dep_graph if isinstance(dep_graph, dict) else {} + + +def _serialized_dep_stats(data: dict[str, Any]) -> dict[str, int]: + dep_graph = _serialized_dep_graph(data) + return { + "nodes": _count_serialized_items(dep_graph.get("nodes", [])), + "edges": _count_serialized_items(dep_graph.get("edges", [])), + } + + +def _serialized_dep_to_rpg_map_size(data: dict[str, Any]) -> int: + dep_to_rpg_map = data.get("_dep_to_rpg_map") + if isinstance(dep_to_rpg_map, dict): + return len(dep_to_rpg_map) + + nodes = _serialized_dep_graph(data).get("nodes", {}) + if isinstance(nodes, dict): + return sum( + 1 + for attrs in nodes.values() + if isinstance(attrs, dict) and attrs.get("rpg_nodes") + ) + if isinstance(nodes, list): + return sum( + 1 + for attrs in nodes + if isinstance(attrs, dict) and attrs.get("rpg_nodes") + ) + return 0 + + def run_update_rpg( rpg_file: str, last_repo_dir: str, @@ -111,7 +178,8 @@ def run_update_rpg( # Record pre-update stats + previous git meta so we can report # how the sync baseline advanced. pre_nodes = len(rpg.nodes) - pre_edges = len(rpg.edges) + pre_edges = _serialized_feature_edges(data) + pre_dep_stats = _serialized_dep_stats(data) pre_commit = (rpg.git_meta or {}).get("head_commit") # === Step 1: LLM-driven feature graph refactor === @@ -184,7 +252,8 @@ def run_update_rpg( # Collect stats post_nodes = len(updated_rpg.nodes) - post_edges = len(updated_rpg.edges) + post_edges = _serialized_feature_edges(result_data) + post_dep_stats = _serialized_dep_stats(result_data) stats = { "repo_name": repo_name, @@ -194,6 +263,11 @@ def run_update_rpg( "edge_count": post_edges, "nodes_delta": post_nodes - pre_nodes, "edges_delta": post_edges - pre_edges, + "dep_nodes": post_dep_stats["nodes"], + "dep_edges": post_dep_stats["edges"], + "dep_nodes_delta": post_dep_stats["nodes"] - pre_dep_stats["nodes"], + "dep_edges_delta": post_dep_stats["edges"] - pre_dep_stats["edges"], + "dep_to_rpg_map_size": _serialized_dep_to_rpg_map_size(result_data), "aligned": enrich_stats.get("aligned", 0), "groups_pathed": enrich_stats.get("groups_pathed", 0), "l1_pathed": enrich_stats.get("l1_pathed", 0), diff --git a/CoderMind/templates/commands/update_rpg.md b/CoderMind/templates/commands/update_rpg.md index edfc78d..1f01d38 100644 --- a/CoderMind/templates/commands/update_rpg.md +++ b/CoderMind/templates/commands/update_rpg.md @@ -45,8 +45,9 @@ Inspect the `type` field in the JSON output: corrupt; the user may need to delete it and rerun `/cmind.encode`. * **`init`** → no `rpg.json` yet. Tell the user to run `/cmind.encode` first to create the baseline graph, then terminate. -* **`update`** → display `result.stats.repo_name` / `node_count` / - `edge_count` and proceed to Step 2. +* **`update`** → display `result.stats.repo_name`, Feature graph + `node_count` / `edge_count`, and Dependency graph `dep_nodes` / + `dep_edges`, then proceed to Step 2. Also verify there is at least one previous commit (the update needs `HEAD~1` as baseline): @@ -80,8 +81,10 @@ not need to redirect output. RPG update complete! Repository: Previous ref: - Nodes: (delta: ) - Edges: (delta: ) + Feature graph Nodes: (delta: ) + Feature graph Edges: (delta: ) + Dependency graph Nodes: (delta: ) + Dependency graph Edges: (delta: ) Aligned to dep_graph: Functional areas: Saved to: diff --git a/CoderMind/tests/test_encode_commands.py b/CoderMind/tests/test_encode_commands.py index 85ca8cb..1e8b5db 100644 --- a/CoderMind/tests/test_encode_commands.py +++ b/CoderMind/tests/test_encode_commands.py @@ -111,7 +111,17 @@ def test_update_state_valid_rpg(self, tmp_path, monkeypatch): cmind_data = tmp_path / ".cmind" / "data" cmind_data.mkdir(parents=True) rpg_file = cmind_data / "rpg.json" - rpg_file.write_text(json.dumps(_make_rpg_data(), indent=2)) + rpg_data = _make_rpg_data() + rpg_data["dep_graph"] = { + "nodes": { + "main.py": {"type": "file"}, + "main.py:hello": {"type": "function"}, + }, + "edges": [ + {"src": "main.py", "dst": "main.py:hello", "attrs": {"type": "contains"}}, + ], + } + rpg_file.write_text(json.dumps(rpg_data, indent=2)) from rpg_encoder.check_encode import check_encode result = check_encode() @@ -120,6 +130,8 @@ def test_update_state_valid_rpg(self, tmp_path, monkeypatch): assert result["stats"]["repo_name"] == "test_repo" assert result["stats"]["node_count"] == 2 assert result["stats"]["edge_count"] == 1 + assert result["stats"]["dep_nodes"] == 2 + assert result["stats"]["dep_edges"] == 1 def test_error_state_invalid_rpg(self, tmp_path, monkeypatch): """When rpg.json exists but has invalid format, return type=error.""" @@ -348,6 +360,8 @@ def test_update_rpg_template_references_check_script(self): content = f.read() assert "check_encode.py" in content assert "update_graphs.py update-rpg" in content + assert "Dependency graph Nodes: (delta: )" in content + assert "Dependency graph Edges: (delta: )" in content # ============================================================================ diff --git a/CoderMind/tests/test_step4_integration.py b/CoderMind/tests/test_step4_integration.py index 8b94eef..115dcad 100644 --- a/CoderMind/tests/test_step4_integration.py +++ b/CoderMind/tests/test_step4_integration.py @@ -388,7 +388,15 @@ def test_run_update_rpg_advances_meta_git_and_runs_align(update_rpg_workspace, m # dep_graph is embedded in rpg.json (single source of truth) assert "dep_graph" in persisted - assert persisted["dep_graph"]["nodes"] + dep_graph = persisted["dep_graph"] + assert dep_graph["nodes"] + assert result["edge_count"] == len(persisted["edges"]) + assert result["nodes_delta"] == 0 + assert result["edges_delta"] == 0 + assert result["dep_nodes"] == len(dep_graph["nodes"]) + assert result["dep_edges"] == len(dep_graph["edges"]) + assert result["dep_nodes_delta"] == 0 + assert result["dep_edges_delta"] == 0 def test_run_update_rpg_dep_graph_path_default_matches_constant(monkeypatch, tmp_path):