Skip to content
Open
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
11 changes: 7 additions & 4 deletions CoderMind/scripts/rpg/graph_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import logging
from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union

from common.rpg_io import safe_load_rpg

Expand Down Expand Up @@ -135,14 +135,17 @@ def from_files(cls, rpg_path: str, dep_graph_path: str = "") -> "GraphQueryEngin
# Helpers
# ------------------------------------------------------------------

def _normalize_path(self, meta_path: str) -> str:
def _normalize_path(self, meta_path: Union[str, List[str]]) -> Union[str, List[str]]:
"""Strip redundant code_dir prefix from RPG meta.path.

For legacy data where ``_dep_graph_code_dir`` was ``"repo"``,
this converts e.g. ``repo/routes/auth.py`` to ``routes/auth.py``
so it aligns with dep_graph node IDs. In the unified
workspace==repo layout the prefix is empty and this is a no-op.
A list path, as stored for a multi-file group, is normalised per entry.
"""
if isinstance(meta_path, list):
return [self._normalize_path(entry) for entry in meta_path]
if self._code_dir_prefix and meta_path.startswith(self._code_dir_prefix):
return meta_path[len(self._code_dir_prefix):]
return meta_path
Expand Down Expand Up @@ -306,8 +309,8 @@ def _search_rpg_tree(self, query: str, top_k: int) -> List[Dict[str, Any]]:
elif query in name.lower():
score = 75
else:
file_part = meta_path.split(":")[0] if ":" in meta_path else meta_path
if query in file_part.lower():
paths = meta_path if isinstance(meta_path, list) else [meta_path]
if any(query in path.split(":")[0].lower() for path in paths):
score = 60
elif query in nid.lower():
score = 55
Expand Down
98 changes: 98 additions & 0 deletions CoderMind/tests/test_graph_query_meta_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Search still works when a feature node's ``meta.path`` is a list.

``enrich_from_code`` stores a list when a group or area spans more than one
file or directory. Search and path normalisation must accept that shape.
"""

import sys
from pathlib import Path

_PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_PROJECT_ROOT))
sys.path.insert(0, str(_PROJECT_ROOT / "scripts"))

from rpg.graph_query import GraphQueryEngine

_LIST_PATH = ["services/inference/router.py", "api/inference/routes.py"]


def _engine(path, code_dir: str = "") -> GraphQueryEngine:
rpg = {
"_dep_graph_code_dir": code_dir,
"root": {
"id": "repo",
"name": "brain",
"node_type": "repo",
"meta": {"path": "."},
"children": [
{
"id": "area",
"name": "Inference Serving",
"node_type": "functional_area",
"meta": {"path": path, "description": "routes a request"},
"children": [],
}
],
},
}
dep = {
"nodes": {
"services/inference/router.py:route": {
"name": "route",
"type": "function",
}
},
"edges": [],
}
return GraphQueryEngine(rpg, dep)


def _area(results: list[dict]) -> dict:
matches = [item for item in results if item.get("id") == "area"]
assert matches, results
return matches[0]


def test_feature_search_matches_a_later_list_entry() -> None:
"""A query in the second path scores the path match, not a crash."""
engine = _engine(_LIST_PATH)
for scope in ("feature", "all"):
hit = _area(engine.search("routes", scope=scope))
assert hit["score"] == 60
assert hit["path"] == _LIST_PATH


def test_string_path_search_is_unchanged() -> None:
"""A string path still matches and comes back as a string."""
path = "services/inference/router.py"
hit = _area(_engine(path).search("router", scope="feature"))
assert hit["score"] == 60
assert hit["path"] == path


def test_normalize_path_strips_prefix_from_each_list_entry() -> None:
"""A legacy code-dir prefix is removed from every path in a list."""
engine = _engine(
["repo/services/inference", "repo/api/inference"],
code_dir="repo",
)
assert engine._normalize_path(["repo/services/inference", "other/api"]) == [
"services/inference",
"other/api",
]
assert engine._normalize_path("repo/services/inference/router.py") == (
"services/inference/router.py"
)


def test_tree_and_detail_keep_list_paths() -> None:
"""Browsing and detail return the same list search does."""
engine = _engine(
["repo/services/inference", "repo/api/inference"],
code_dir="repo",
)
expected = ["services/inference", "api/inference"]
tree = engine.list_tree(max_depth=2)
area = tree["children"][0]
assert area["path"] == expected
assert engine.get_node_detail("area")["code_path"] == expected