Skip to content

Commit 2c4d1c2

Browse files
authored
Merge pull request #17 from closedloop-ai/campaign-prd-739-20260915-c2
feat(code-intel): relations keeps its completeness fields (PLN-2027 PR 2)
2 parents 0cbb4c8 + e44bbbf commit 2c4d1c2

6 files changed

Lines changed: 295 additions & 19 deletions

File tree

src/lemoncrow/core/capabilities/code_context_contract.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,17 +182,36 @@ class CallGraphEdge(BaseModel):
182182

183183

184184
class CallGraphTraversalResult(BaseModel):
185-
"""Traversal output plus cheap snapshot metadata."""
185+
"""Traversal output plus cheap snapshot metadata.
186+
187+
``nodes`` is what survived ``limit``. ``related_symbol_ids`` is every distinct
188+
related symbol the traversal saw, kept or not, so :attr:`related_total` is
189+
counted before the limit, and a multi-target merge can union it without
190+
double-counting a caller that same-named targets share. The ids are excluded
191+
from serialization: consumers read the count.
192+
193+
``related_total_exact`` is false when that count is only a lower bound: a
194+
``depth`` > 1 walk was cut (symbols past the cap were never expanded), a
195+
neighbour lookup stopped at its own row ceiling, or there was no edge data to
196+
count.
197+
"""
186198

187199
model_config = ConfigDict(extra="forbid")
188200

189201
nodes: list[CallGraphNode]
190202
edges: list[CallGraphEdge]
203+
related_symbol_ids: list[str] = Field(exclude=True)
204+
related_total_exact: bool
191205
truncated: bool = False
192206
data_status: CallGraphDataStatus = "available"
193207
message: str | None = None
194208
snapshot: dict[str, Any] | None = None
195209

210+
@property
211+
def related_total(self) -> int:
212+
"""Distinct related symbols found before ``limit`` was applied."""
213+
return len(self.related_symbol_ids)
214+
196215

197216
# --------------------------------------------------------------------------- #
198217
# Provider interface

src/lemoncrow/gateway/adapters/mcp_server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10995,6 +10995,13 @@ def tool_relations(
1099510995
(default usages); `depth` extends callers/callees transitively. The COUNTS for
1099610996
these already ride along on `grep` definition matches — use this only to expand
1099710997
a count into the concrete list.
10998+
10999+
callers/callees: `related_count` is the rows returned; `related_total` is the
11000+
distinct related symbols found before `limit`, a lower bound when
11001+
`related_total_exact` is false. Rows are enclosing-symbol spans (the calling or
11002+
called definition), not call sites.
11003+
usages: `reference_count` is the rows returned.
11004+
`truncated` (and, for callers/callees, both totals) survives response trimming.
1099811005
"""
1099911006
target = _parse_symbol(symbol)
1100011007
rel = kind.strip().lower()

src/lemoncrow/pro/capabilities/code_context/call_graph.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,28 @@ def traverse_call_graph(
4343
limit: int,
4444
lookup_neighbors: Callable[[str], list[CallGraphNode] | None],
4545
snapshot: bool = False,
46+
neighbor_cap: int | None = None,
4647
) -> CallGraphTraversalResult:
47-
"""Traverse routed callers/callees with cycle-safe breadth-first expansion."""
48+
"""Traverse routed callers/callees with cycle-safe breadth-first expansion.
49+
50+
Symbols past *limit* are still counted, though never kept or expanded, so the
51+
result's ``related_total`` is taken before the limit. At depth 1 every
52+
neighbour of the target is seen and the count is exact; past depth 1 a cut
53+
walk never expands what it dropped, so the count is a lower bound.
54+
55+
*neighbor_cap* is the lookup's own row ceiling. A lookup that returns that
56+
many rows may have stopped early, so its neighbour set is treated as cut.
57+
"""
4858

4959
target_symbol_id = str(target["symbol_id"])
5060
queue: deque[tuple[str, int]] = deque([(target_symbol_id, 1)])
5161
visited: set[str] = {target_symbol_id}
5262
nodes_by_id: dict[str, CallGraphNode] = {}
63+
related_ids: set[str] = set()
5364
edge_keys: set[tuple[str, str, int]] = set()
5465
edges: list[CallGraphEdge] = []
5566
truncated = False
67+
lookup_capped = False
5668

5769
while queue:
5870
current_symbol_id, current_depth = queue.popleft()
@@ -61,11 +73,17 @@ def traverse_call_graph(
6173
return CallGraphTraversalResult(
6274
nodes=[],
6375
edges=[],
76+
# Nothing could be looked up, so zero is not a count.
77+
related_symbol_ids=[],
78+
related_total_exact=False,
6479
truncated=False,
6580
data_status="unavailable",
6681
message="routed call edge data is unavailable",
6782
snapshot=None,
6883
)
84+
if neighbor_cap is not None and len(neighbors) >= neighbor_cap:
85+
lookup_capped = True
86+
truncated = True
6987
for neighbor in neighbors:
7088
if direction == "callers":
7189
edge_key = (neighbor.symbol_id, current_symbol_id, current_depth)
@@ -86,6 +104,7 @@ def traverse_call_graph(
86104
edges.append(edge)
87105
if neighbor.symbol_id == target_symbol_id:
88106
continue
107+
related_ids.add(neighbor.symbol_id)
89108
if neighbor.symbol_id not in nodes_by_id:
90109
if len(nodes_by_id) >= limit:
91110
truncated = True
@@ -112,6 +131,8 @@ def traverse_call_graph(
112131
return CallGraphTraversalResult(
113132
nodes=ordered_nodes,
114133
edges=ordered_edges,
134+
related_symbol_ids=sorted(related_ids),
135+
related_total_exact=not lookup_capped and (depth <= 1 or not truncated),
115136
truncated=truncated,
116137
data_status=data_status,
117138
message=None if ordered_edges else "no related call edges were found",
@@ -126,7 +147,12 @@ def build_call_graph_payload(
126147
depth: int,
127148
result: CallGraphTraversalResult,
128149
) -> dict[str, Any]:
129-
"""Shape the public callers/callees response payload."""
150+
"""Shape the public callers/callees response payload.
151+
152+
``related_count`` is the rows returned. ``related_total`` is the distinct
153+
related symbols found before ``limit``; ``related_total_exact`` says whether
154+
that count is exact or a lower bound.
155+
"""
130156

131157
return {
132158
"target": summarize_symbol(target),
@@ -135,6 +161,8 @@ def build_call_graph_payload(
135161
"related": [item.model_dump(mode="json") for item in result.nodes],
136162
"edges": [item.model_dump(mode="json") for item in result.edges],
137163
"related_count": len(result.nodes),
164+
"related_total": result.related_total,
165+
"related_total_exact": result.related_total_exact,
138166
"edge_count": len(result.edges),
139167
"truncated": result.truncated,
140168
"data_status": result.data_status,

src/lemoncrow/pro/capabilities/code_context/engine.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -691,21 +691,28 @@ def _explore_skeleton_enabled() -> bool:
691691
"direction",
692692
"related",
693693
"related_count",
694+
# A consumer decides whether the list is whole from these, so trimming a
695+
# large response must never drop them.
696+
"truncated",
697+
"related_total",
698+
"related_total_exact",
694699
"data_status",
695700
"provenance",
696701
]
697702
_CALL_GRAPH_OPTIONAL_KEYS = [
698703
"depth",
699704
"related",
700705
"related_count",
701-
"truncated",
702706
"edges",
703707
"edge_count",
704708
"data_status",
705709
"ambiguity",
706710
"message",
707711
"snapshot",
708712
]
713+
# Row ceiling of one symbol's caller lookup (`_find_callers_local`). A lookup that
714+
# returns this many rows may have stopped early, so traversal treats it as cut.
715+
_CALLER_LOOKUP_ROW_CAP = 1000
709716
_BLAME_ESSENTIAL_KEYS = [
710717
"symbol_name",
711718
"file_path",
@@ -7325,7 +7332,9 @@ def tool_callers_batch(
73257332

73267333
placeholders = ",".join("?" for _ in names)
73277334
target_rows: dict[str, list[sqlite3.Row]] = {name: [] for name in names}
7328-
caller_rows: dict[str, list[sqlite3.Row]] = {name: [] for name in names}
7335+
# Kept rows are keyed by caller symbol id; ``related_ids`` counts every caller, kept or not.
7336+
caller_rows: dict[str, dict[str, sqlite3.Row]] = {name: {} for name in names}
7337+
related_ids: dict[str, set[str]] = {name: set() for name in names}
73297338
with self._connect() as conn:
73307339
self._init_schema(conn)
73317340
rows = conn.execute(
@@ -7366,11 +7375,24 @@ def tool_callers_batch(
73667375
""",
73677376
(self.repo_id, *names),
73687377
).fetchall()
7369-
# Keep one extra unique row per name to preserve truncation metadata.
7378+
# The edge query has no LIMIT, so every caller is counted before the limit.
7379+
# Keep one extra unique caller per name to preserve truncation metadata.
73707380
for row in edge_rows:
73717381
name = str(row["callee_short_name"])
7372-
if name in caller_rows and len(caller_rows[name]) <= bounded_limit:
7373-
caller_rows[name].append(row)
7382+
if name not in caller_rows:
7383+
continue
7384+
hydrated_id = row["hydrated_symbol_id"]
7385+
if hydrated_id is not None:
7386+
caller_id = str(hydrated_id)
7387+
else:
7388+
cf = str(row["caller_file_path"])
7389+
cs = int(row["caller_start_line"])
7390+
cq = str(row["caller_qualified_name"])
7391+
caller_id = "local-call::" + hashlib.sha1(f"{cf}:{cs}:{cq}".encode()).hexdigest()[:16]
7392+
related_ids[name].add(caller_id)
7393+
kept = caller_rows[name]
7394+
if caller_id not in kept and len(kept) <= bounded_limit:
7395+
kept[caller_id] = row
73747396

73757397
payloads: dict[str, dict[str, Any]] = {}
73767398
for name in names:
@@ -7382,15 +7404,14 @@ def tool_callers_batch(
73827404
symbol = _row_to_symbol(dict(row))
73837405
targets.append({**symbol.model_dump(mode="json"), "provenance": _LOCAL_PROVENANCE})
73847406
nodes_by_id: dict[str, CallGraphNode] = {}
7385-
for row in caller_rows.get(name, ()):
7407+
for caller_id, row in caller_rows[name].items():
73867408
cf = str(row["caller_file_path"])
73877409
cs = int(row["caller_start_line"])
73887410
cn = str(row["caller_symbol_name"])
73897411
cq = str(row["caller_qualified_name"])
7390-
hydrated_id = row["hydrated_symbol_id"]
7391-
if hydrated_id is not None:
7412+
if row["hydrated_symbol_id"] is not None:
73927413
node = CallGraphNode(
7393-
symbol_id=str(hydrated_id),
7414+
symbol_id=caller_id,
73947415
symbol_name=cn,
73957416
qualified_name=str(row["hydrated_qualified_name"] or cq),
73967417
file_path=cf,
@@ -7400,9 +7421,8 @@ def tool_callers_batch(
74007421
provenance="local_index",
74017422
)
74027423
else:
7403-
synthetic_id = "local-call::" + hashlib.sha1(f"{cf}:{cs}:{cq}".encode()).hexdigest()[:16]
74047424
node = CallGraphNode(
7405-
symbol_id=synthetic_id,
7425+
symbol_id=caller_id,
74067426
symbol_name=cn,
74077427
qualified_name=cq,
74087428
file_path=cf,
@@ -7411,7 +7431,7 @@ def tool_callers_batch(
74117431
end_line=int(row["caller_end_line"]),
74127432
provenance="local_index",
74137433
)
7414-
nodes_by_id.setdefault(node.symbol_id, node)
7434+
nodes_by_id[caller_id] = node
74157435

74167436
all_nodes = sorted(nodes_by_id.values(), key=lambda item: (item.file_path, item.start_line, item.symbol_id))
74177437
truncated = len(all_nodes) > bounded_limit
@@ -7426,6 +7446,9 @@ def tool_callers_batch(
74267446
traversal = CallGraphTraversalResult(
74277447
nodes=nodes,
74287448
edges=edges,
7449+
# Every call edge for the name was read, so the pre-limit count is exact.
7450+
related_symbol_ids=sorted(related_ids[name]),
7451+
related_total_exact=True,
74297452
truncated=truncated,
74307453
data_status="available" if edges else "empty",
74317454
message=None if edges else "no related call edges were found",
@@ -10599,6 +10622,7 @@ def _tool_call_graph(
1059910622
limit=limit,
1060010623
snapshot=snapshot,
1060110624
lookup_neighbors=lambda current_symbol_id: lookup(symbol_id=current_symbol_id),
10625+
neighbor_cap=_CALLER_LOOKUP_ROW_CAP if direction == "callers" else None,
1060210626
)
1060310627
if traversal.data_status == "unavailable" and direction == "callers":
1060410628
fallback = self._fallback_callers_from_references(
@@ -10614,10 +10638,16 @@ def _tool_call_graph(
1061410638
nodes_by_identity: dict[tuple[str, str, int, int, str], CallGraphNode] = {}
1061510639
edges_by_key: dict[tuple[str, str, int], CallGraphEdge] = {}
1061610640
merged_truncated = False
10641+
# The edge store is name-keyed, so same-named targets share callers:
10642+
# union the totals by symbol id, never sum them.
10643+
related_ids: set[str] = set()
10644+
merged_exact = True
1061710645
status_rank = {"unavailable": 0, "empty": 1, "available": 2}
1061810646
merged_status = "unavailable"
1061910647
for current in traversals:
1062010648
merged_truncated = merged_truncated or current.truncated
10649+
related_ids.update(current.related_symbol_ids)
10650+
merged_exact = merged_exact and current.related_total_exact
1062110651
if status_rank[current.data_status] > status_rank[merged_status]:
1062210652
merged_status = current.data_status
1062310653
for node in current.nodes:
@@ -10660,6 +10690,8 @@ def _tool_call_graph(
1066010690
traversal = CallGraphTraversalResult(
1066110691
nodes=merged_nodes,
1066210692
edges=merged_edges,
10693+
related_symbol_ids=sorted(related_ids),
10694+
related_total_exact=merged_exact,
1066310695
truncated=merged_truncated,
1066410696
data_status=cast(Any, merged_status),
1066510697
message=merged_message,
@@ -10684,6 +10716,7 @@ def _tool_call_graph(
1068410716
edges_before = len(cast(list[dict[str, Any]], payload.get("edges", [])))
1068510717
payload["related"] = cast(list[dict[str, Any]], payload.get("related", []))[:max_related]
1068610718
payload["edges"] = cast(list[dict[str, Any]], payload.get("edges", []))[:max_related]
10719+
# Only the returned rows are recounted; related_total stays the pre-limit count.
1068710720
payload["related_count"] = len(cast(list[dict[str, Any]], payload.get("related", [])))
1068810721
payload["edge_count"] = len(cast(list[dict[str, Any]], payload.get("edges", [])))
1068910722
payload["truncated"] = (
@@ -12837,9 +12870,9 @@ def _find_callers_local(
1283712870
FROM call_edges
1283812871
WHERE repo_id = ? AND callee_short_name = ?
1283912872
ORDER BY caller_file_path, caller_start_line
12840-
LIMIT 1000
12873+
LIMIT ?
1284112874
""",
12842-
(self.repo_id, target_name),
12875+
(self.repo_id, target_name, _CALLER_LOOKUP_ROW_CAP),
1284312876
).fetchall()
1284412877
if not rows:
1284512878
return []
@@ -13060,13 +13093,15 @@ def _fallback_callers_from_references(
1306013093
nodes_by_id: dict[str, CallGraphNode] = {}
1306113094
edges: list[CallGraphEdge] = []
1306213095
seen_edges: set[tuple[str, str, int]] = set()
13096+
related_ids: set[str] = set()
1306313097
truncated = False
1306413098
for reference in references:
1306513099
if reference.file_path == target_file and target_start <= reference.line <= target_end:
1306613100
continue
1306713101
node = self._caller_node_from_reference(reference, target_symbol_id=target_symbol_id)
1306813102
if node is None:
1306913103
continue
13104+
related_ids.add(node.symbol_id)
1307013105
if node.symbol_id not in nodes_by_id:
1307113106
if len(nodes_by_id) >= limit:
1307213107
truncated = True
@@ -13088,6 +13123,8 @@ def _fallback_callers_from_references(
1308813123
return CallGraphTraversalResult(
1308913124
nodes=[],
1309013125
edges=[],
13126+
related_symbol_ids=[],
13127+
related_total_exact=False,
1309113128
truncated=False,
1309213129
data_status="unavailable",
1309313130
message="routed call edge data is unavailable",
@@ -13096,6 +13133,10 @@ def _fallback_callers_from_references(
1309613133
return CallGraphTraversalResult(
1309713134
nodes=ordered_nodes,
1309813135
edges=ordered_edges,
13136+
related_symbol_ids=sorted(related_ids),
13137+
# References come through a row-capped lookup whose saturation is not
13138+
# visible here, so this count is never claimed exact.
13139+
related_total_exact=False,
1309913140
truncated=truncated,
1310013141
data_status="available",
1310113142
message="fallback caller graph derived from symbol references",

0 commit comments

Comments
 (0)