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
10 changes: 3 additions & 7 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1739,13 +1739,9 @@ def dispatch_command(cmd: str) -> None:
sys.exit(1)
nid = matches[0]
d = G.nodes[nid]
print(f"Node: {d.get('label', nid)}")
print(f" ID: {nid}")
print(
f" Source: {d.get('source_file', '')} {d.get('source_location', '')}".rstrip()
)
print(f" Type: {d.get('file_type', '')}")
print(f" Community: {d.get('community_name') or d.get('community', '')}")
from graphify.serve import _format_node_detail_lines
for line in _format_node_detail_lines(nid, d):
print(line)
# Work-memory overlay: a derived experiential hint from `graphify reflect`,
# merged in display-only from the .graphify_learning.json sidecar next to
# graph.json. No line when the node has no overlay entry.
Expand Down
95 changes: 75 additions & 20 deletions graphify/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -1048,8 +1048,8 @@ def _adj(n):
f"NODE {sanitize_label(d.get('label', nid))} "
f"[src={sanitize_label(str(d.get('source_file', '')))} "
f"loc={sanitize_label(str(d.get('source_location', '')))} "
f"community={sanitize_label(str(d.get('community_name') or d.get('community', '')))}"
f"{learning_suffix}]"
f"community={_resolved_community_label(d)}"
f"{_node_description_suffix(d)}{learning_suffix}]"
)
lines.append(line)
for u, v in edges:
Expand Down Expand Up @@ -1525,6 +1525,71 @@ def _relay() -> None:
sys.stdin = open(0, "r", closefd=False)


def _resolved_community_label(d: dict) -> str:
name = d.get("community_name")
if name:
return sanitize_label(str(name))
cid = d.get("community")
return sanitize_label(str(cid)) if cid is not None else ""


def _node_description_suffix(d: dict) -> str:
desc = d.get("description")
if not desc:
return ""
return f" desc={sanitize_label(str(desc))}"


def _detail_field(label: str, value: str, *, compact: bool) -> str:
if compact:
return f" {label}: {value}"
padding = {
"ID": " ",
"Source": " ",
"Defined in": " ",
"Type": " ",
"Community": " ",
"Description": " ",
"Degree": " ",
}
return f" {label}:{padding.get(label, ' ')}{value}"


def _format_node_detail_lines(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_format_node_detail_lines()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread
akshitj11 marked this conversation as resolved.
nid: str,
d: dict,
*,
degree: int | None = None,
compact: bool = False,
) -> list[str]:
source = (
f"{sanitize_label(str(d.get('source_file', '')))} "
f"{sanitize_label(str(d.get('source_location', '')))}"
).strip()
lines = [
f"Node: {sanitize_label(d.get('label', nid))}",
_detail_field("ID", sanitize_label(nid), compact=compact),
_detail_field("Source", source, compact=compact),
]
def_file = d.get("definition_file")
if def_file:
defined = (
f"{sanitize_label(str(def_file))} "
f"{sanitize_label(str(d.get('definition_location', '')))}"
).strip()
lines.append(_detail_field("Defined in", defined, compact=compact))
lines.extend([
_detail_field("Type", sanitize_label(str(d.get('file_type', ''))), compact=compact),
_detail_field("Community", _resolved_community_label(d), compact=compact),
])
desc = d.get("description")
if desc:
lines.append(_detail_field("Description", sanitize_label(str(desc)), compact=compact))
if degree is not None:
lines.append(_detail_field("Degree", str(degree), compact=compact))
return lines


def _community_header(cid: int, community_name) -> str:
# Header for get_community: "Community N — Name", matching get_node / query
# output which read the community_name attribute to_json writes onto nodes.
Expand Down Expand Up @@ -1798,21 +1863,7 @@ def _tool_get_node(arguments: dict) -> str:
if err:
return err
d = G.nodes[nid]
# Sanitise every LLM-derived field before concatenation (F-010).
return "\n".join([
f"Node: {sanitize_label(d.get('label', nid))}",
f" ID: {sanitize_label(nid)}",
f" Source: {sanitize_label(str(d.get('source_file', '')))} {sanitize_label(str(d.get('source_location', '')))}",
# A C/C++/ObjC symbol declared in a header and defined in the sibling
# impl file is ONE node keyed to the header, so Source alone points at
# the declaration. Name where it is implemented too, when known.
*([f" Defined in: {sanitize_label(str(d.get('definition_file', '')))} "
f"{sanitize_label(str(d.get('definition_location', '')))}"]
if d.get("definition_file") else []),
f" Type: {sanitize_label(str(d.get('file_type', '')))}",
f" Community: {sanitize_label(str(d.get('community_name') or d.get('community', '')))}",
f" Degree: {G.degree(nid)}",
])
return "\n".join(_format_node_detail_lines(nid, d, degree=G.degree(nid), compact=True))

def _tool_get_neighbors(arguments: dict) -> str:
Comment thread
akshitj11 marked this conversation as resolved.
Comment thread
akshitj11 marked this conversation as resolved.
Comment thread
akshitj11 marked this conversation as resolved.
label = arguments["label"].lower()
Expand All @@ -1835,7 +1886,8 @@ def _edge_at(d: dict) -> str:
if rel_filter and rel_filter not in rel.lower():
continue
lines.append(
f" --> {sanitize_label(G.nodes[nb].get('label', nb))} "
f" --> {sanitize_label(G.nodes[nb].get('label', nb))}"
f"{_node_description_suffix(G.nodes[nb])} "
f"[{sanitize_label(str(rel))}] [{sanitize_label(str(d.get('confidence', '')))}]{_edge_at(d)}"
)
for nb in G.predecessors(nid):
Expand All @@ -1844,7 +1896,8 @@ def _edge_at(d: dict) -> str:
if rel_filter and rel_filter not in rel.lower():
continue
lines.append(
f" <-- {sanitize_label(G.nodes[nb].get('label', nb))} "
f" <-- {sanitize_label(G.nodes[nb].get('label', nb))}"
f"{_node_description_suffix(G.nodes[nb])} "
f"[{sanitize_label(str(rel))}] [{sanitize_label(str(d.get('confidence', '')))}]{_edge_at(d)}"
)
budget = int(arguments.get("token_budget", 2000))
Expand All @@ -1864,7 +1917,9 @@ def _tool_get_community(arguments: dict) -> str:
# Sanitise label and source_file (F-010).
lines.append(
f" {sanitize_label(d.get('label', n))} "
f"[{sanitize_label(str(d.get('source_file', '')))}]"
f"[{sanitize_label(str(d.get('source_file', '')))}] "
f"community={_resolved_community_label(d)}"
f"{_node_description_suffix(d)}"
)
budget = int(arguments.get("token_budget", 2000))
return _cut_lines_to_budget(
Expand Down
18 changes: 18 additions & 0 deletions tests/test_explain_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ def test_explain_no_lesson_line_for_unannotated_node(monkeypatch, tmp_path, caps
assert "Lesson:" not in out


def test_explain_prints_description(monkeypatch, tmp_path, capsys):
graph_data = {
"directed": False, "multigraph": False, "graph": {},
"nodes": [
{"id": "validate", "label": "validateSanitySession()",
"source_file": "server/sanity-validate-session.ts",
"community": 0, "community_name": "Session",
"description": "Validates the active sanity session token."},
],
"links": [],
}
p = tmp_path / "graph.json"
p.write_text(json.dumps(graph_data))
out = _run(monkeypatch, p, "validateSanitySession", capsys)
assert "Description: Validates the active sanity session token." in out
assert "Community: Session" in out


def test_explain_connection_shows_call_site_line(monkeypatch, tmp_path, capsys):
"""BUG1: an explain connection shows the edge's call-SITE line (in the
caller's file), not the caller's def line."""
Expand Down
95 changes: 95 additions & 0 deletions tests/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
_cut_lines_to_budget,
_load_graph,
_community_header,
_resolved_community_label,
_format_node_detail_lines,
_node_description_suffix,
_search_tokens,
_shortest_path_text,
)
Expand Down Expand Up @@ -655,6 +658,98 @@ def test_subgraph_to_text_no_overlay_is_unchanged():
assert "learning=" not in text


def test_subgraph_to_text_includes_description():
G = _make_graph()
G.nodes["n1"]["description"] = "auth handler"
text = _subgraph_to_text(G, {"n1"}, [])
assert "desc=auth handler" in text


def test_subgraph_to_text_description_counts_against_budget():
G = nx.Graph()
G.add_node("n1", label="x", source_file="a.py", source_location="L1", community=0,
description="z" * 500)
bare = _subgraph_to_text(G, {"n1"}, [], token_budget=50)
assert "truncated" not in bare
annotated = _subgraph_to_text(G, {"n1"}, [], token_budget=50)
assert "truncated" in annotated or len(annotated) < len(bare) + 100


def test_subgraph_to_text_no_description_unchanged_shape():
G = _make_graph()
text = _subgraph_to_text(G, {"n1"}, [])
assert " desc=" not in text
assert "community=0" in text


def test_resolved_community_label_preserves_stored_name():
d = {"community": 2, "community_name": "Community 2"}
assert _resolved_community_label(d) == "Community 2"
d["community_name"] = "Services"
assert _resolved_community_label(d) == "Services"
del d["community_name"]
assert _resolved_community_label(d) == "2"


def test_format_node_detail_lines_mcp_compact_matches_v8():
d = {
"label": "foo",
"source_file": "a.py",
"source_location": "L1",
"file_type": "code",
"community": 1,
"community_name": "Core",
"description": "handles auth",
}
lines = _format_node_detail_lines("foo_id", d, degree=3, compact=True)
assert lines == [
"Node: foo",
" ID: foo_id",
" Source: a.py L1",
" Type: code",
" Community: Core",
" Description: handles auth",
" Degree: 3",
]


def test_format_node_detail_lines_includes_definition_file():
d = {
"label": "parse",
"source_file": "parse.h",
"source_location": "L10",
"definition_file": "parse.cpp",
"definition_location": "L42",
"file_type": "code",
"community": 0,
}
lines = _format_node_detail_lines("parse", d, compact=True)
assert " Defined in: parse.cpp L42" in lines
assert lines.index(" Source: parse.h L10") < lines.index(" Defined in: parse.cpp L42")
assert lines.index(" Defined in: parse.cpp L42") < lines.index(" Type: code")


def test_format_node_detail_lines_includes_description():
d = {
"label": "foo",
"source_file": "a.py",
"source_location": "L1",
"file_type": "code",
"community": 1,
"community_name": "Core",
"description": "handles auth",
}
lines = _format_node_detail_lines("foo_id", d, degree=3)
assert any("Description: handles auth" in line for line in lines)
assert any("Community: Core" in line for line in lines)
assert any("Degree: 3" in line for line in lines)


def test_node_description_suffix_empty_without_field():
assert _node_description_suffix({}) == ""
assert _node_description_suffix({"description": "x"}) == " desc=x"


def test_query_graph_text_explicit_context_filter_changes_traversal():
G = _make_graph()
text = _query_graph_text(G, "extract", mode="bfs", depth=2, token_budget=2000, context_filters=["call"])
Expand Down
Loading