Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CoderMind/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
40 changes: 26 additions & 14 deletions CoderMind/scripts/rpg_encoder/check_encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,36 @@ 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] = {}
stats["repo_name"] = data.get("repo_name", "unknown")

# 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", {})
Expand All @@ -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")
Expand Down
78 changes: 76 additions & 2 deletions CoderMind/scripts/rpg_encoder/run_update_rpg.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import sys
import argparse
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

Expand All @@ -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,
Expand Down Expand Up @@ -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 ===
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand Down
11 changes: 7 additions & 4 deletions CoderMind/templates/commands/update_rpg.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -80,8 +81,10 @@ not need to redirect output.
RPG update complete!
Repository: <repo_name>
Previous ref: <prev_ref>
Nodes: <node_count> (delta: <nodes_delta>)
Edges: <edge_count> (delta: <edges_delta>)
Feature graph Nodes: <node_count> (delta: <nodes_delta>)
Feature graph Edges: <edge_count> (delta: <edges_delta>)
Dependency graph Nodes: <dep_nodes> (delta: <dep_nodes_delta>)
Dependency graph Edges: <dep_edges> (delta: <dep_edges_delta>)
Aligned to dep_graph: <aligned>
Functional areas: <functional_areas>
Saved to: <output_path>
Expand Down
16 changes: 15 additions & 1 deletion CoderMind/tests/test_encode_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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."""
Expand Down Expand Up @@ -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: <dep_nodes> (delta: <dep_nodes_delta>)" in content
assert "Dependency graph Edges: <dep_edges> (delta: <dep_edges_delta>)" in content


# ============================================================================
Expand Down
10 changes: 9 additions & 1 deletion CoderMind/tests/test_step4_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading