coderay 1.0.6__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coderay-1.0.6/src/coderay.egg-info → coderay-1.0.7}/PKG-INFO +1 -1
- {coderay-1.0.6 → coderay-1.0.7}/pyproject.toml +1 -1
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/models.py +19 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/code_graph.py +64 -17
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/extractor.py +8 -10
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/mcp_server/server.py +9 -5
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/models.py +3 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/skeleton/extractor.py +18 -3
- {coderay-1.0.6 → coderay-1.0.7/src/coderay.egg-info}/PKG-INFO +1 -1
- {coderay-1.0.6 → coderay-1.0.7}/LICENSE +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/README.md +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/setup.cfg +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/chunking/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/chunking/chunker.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/cli/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/cli/commands.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/config.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/lock.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/timing.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/utils.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/embedding/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/embedding/base.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/embedding/local.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/builder.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/mcp_server/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/mcp_server/errors.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/parsing/base.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/parsing/languages.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/pipeline/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/pipeline/indexer.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/pipeline/watcher.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/boosting.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/search.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/skeleton/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/state/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/state/machine.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/state/version.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/storage/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/storage/lancedb.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/vcs/__init__.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay/vcs/git.py +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/SOURCES.txt +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/dependency_links.txt +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/entry_points.txt +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/requires.txt +0 -0
- {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/top_level.txt +0 -0
|
@@ -68,3 +68,22 @@ class GraphEdge:
|
|
|
68
68
|
source: str
|
|
69
69
|
target: str
|
|
70
70
|
kind: EdgeKind
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ImpactResult:
|
|
75
|
+
"""Result of an impact-radius query with resolution diagnostics."""
|
|
76
|
+
|
|
77
|
+
resolved_node: str | None
|
|
78
|
+
nodes: list[GraphNode]
|
|
79
|
+
hint: str | None = None
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict:
|
|
82
|
+
"""Serialize to a JSON-compatible dict."""
|
|
83
|
+
d: dict = {
|
|
84
|
+
"resolved_node": self.resolved_node,
|
|
85
|
+
"results": [n.to_dict() for n in self.nodes],
|
|
86
|
+
}
|
|
87
|
+
if self.hint:
|
|
88
|
+
d["hint"] = self.hint
|
|
89
|
+
return d
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections import defaultdict
|
|
4
|
+
from collections import defaultdict, deque
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
import networkx as nx
|
|
8
8
|
|
|
9
|
-
from coderay.core.models import EdgeKind, GraphEdge, GraphNode, NodeKind
|
|
9
|
+
from coderay.core.models import EdgeKind, GraphEdge, GraphNode, ImpactResult, NodeKind
|
|
10
10
|
from coderay.parsing.languages import (
|
|
11
11
|
get_init_filenames,
|
|
12
12
|
get_resolution_suffixes,
|
|
@@ -280,29 +280,76 @@ class CodeGraph:
|
|
|
280
280
|
# Public query methods
|
|
281
281
|
# ------------------------------------------------------------------
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
_IMPACT_EDGE_KINDS = frozenset(
|
|
284
|
+
{EdgeKind.CALLS, EdgeKind.IMPORTS, EdgeKind.INHERITS}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def get_impact_radius(
|
|
288
|
+
self, symbol: str, depth: int = 2
|
|
289
|
+
) -> ImpactResult:
|
|
284
290
|
"""Find all nodes that could be affected if ``symbol`` changes.
|
|
285
291
|
|
|
292
|
+
Traverses predecessors via CALLS, IMPORTS, and INHERITS edges
|
|
293
|
+
only. DEFINES edges (containment) are excluded because a
|
|
294
|
+
module defining a symbol is not "affected" by changes to it.
|
|
295
|
+
|
|
286
296
|
Args:
|
|
287
|
-
|
|
288
|
-
|
|
297
|
+
symbol: Fully qualified node ID or resolvable name.
|
|
298
|
+
depth: Number of reverse-BFS hops.
|
|
289
299
|
"""
|
|
290
300
|
resolved = self.resolve_symbol(symbol) or symbol
|
|
291
301
|
if resolved not in self._g:
|
|
292
|
-
return
|
|
302
|
+
return self._not_found_result(symbol)
|
|
303
|
+
|
|
293
304
|
visited: set[str] = set()
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
queue: deque[tuple[str, int]] = deque(
|
|
306
|
+
(pred, 1)
|
|
307
|
+
for pred in self._g.predecessors(resolved)
|
|
308
|
+
if self._is_impact_edge(pred, resolved)
|
|
309
|
+
)
|
|
310
|
+
while queue:
|
|
311
|
+
nid, hop = queue.popleft()
|
|
312
|
+
if nid in visited:
|
|
313
|
+
continue
|
|
314
|
+
visited.add(nid)
|
|
315
|
+
if hop < depth and nid in self._g:
|
|
300
316
|
for pred in self._g.predecessors(nid):
|
|
301
|
-
if
|
|
302
|
-
visited
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
317
|
+
if (
|
|
318
|
+
pred not in visited
|
|
319
|
+
and self._is_impact_edge(pred, nid)
|
|
320
|
+
):
|
|
321
|
+
queue.append((pred, hop + 1))
|
|
322
|
+
|
|
323
|
+
nodes = [
|
|
324
|
+
self.get_node(nid)
|
|
325
|
+
for nid in visited
|
|
326
|
+
if self.get_node(nid) is not None
|
|
327
|
+
]
|
|
328
|
+
return ImpactResult(resolved_node=resolved, nodes=nodes)
|
|
329
|
+
|
|
330
|
+
def _is_impact_edge(self, source: str, target: str) -> bool:
|
|
331
|
+
"""Check whether the edge is a dependency (not containment)."""
|
|
332
|
+
kind = self._g.edges[source, target].get("kind")
|
|
333
|
+
return kind in self._IMPACT_EDGE_KINDS
|
|
334
|
+
|
|
335
|
+
def _not_found_result(self, symbol: str) -> ImpactResult:
|
|
336
|
+
"""Build an ImpactResult with diagnostic hints for a missing node."""
|
|
337
|
+
file_part = (
|
|
338
|
+
symbol.split("::")[0] if "::" in symbol else None
|
|
339
|
+
)
|
|
340
|
+
available = (
|
|
341
|
+
sorted(self._file_index.get(file_part, set()))
|
|
342
|
+
if file_part
|
|
343
|
+
else []
|
|
344
|
+
)
|
|
345
|
+
hint = f"Node '{symbol}' not in the graph."
|
|
346
|
+
if available:
|
|
347
|
+
hint += (
|
|
348
|
+
f" Available nodes in {file_part}: {available}"
|
|
349
|
+
)
|
|
350
|
+
return ImpactResult(
|
|
351
|
+
resolved_node=None, nodes=[], hint=hint
|
|
352
|
+
)
|
|
306
353
|
|
|
307
354
|
@property
|
|
308
355
|
def node_count(self) -> int:
|
|
@@ -380,12 +380,11 @@ class GraphTreeSitterParser(BaseTreeSitterParser):
|
|
|
380
380
|
qualified_name=qualified,
|
|
381
381
|
)
|
|
382
382
|
)
|
|
383
|
+
definer = self._module_id
|
|
384
|
+
if scope_stack:
|
|
385
|
+
definer = f"{self.file_path}::{'.'.join(scope_stack)}"
|
|
383
386
|
self._edges.append(
|
|
384
|
-
GraphEdge(
|
|
385
|
-
source=self._module_id,
|
|
386
|
-
target=node_id,
|
|
387
|
-
kind=EdgeKind.DEFINES,
|
|
388
|
-
)
|
|
387
|
+
GraphEdge(source=definer, target=node_id, kind=EdgeKind.DEFINES)
|
|
389
388
|
)
|
|
390
389
|
|
|
391
390
|
# Register in FileContext so calls resolve to the full node_id.
|
|
@@ -422,12 +421,11 @@ class GraphTreeSitterParser(BaseTreeSitterParser):
|
|
|
422
421
|
qualified_name=qualified,
|
|
423
422
|
)
|
|
424
423
|
)
|
|
424
|
+
definer = self._module_id
|
|
425
|
+
if scope_stack:
|
|
426
|
+
definer = f"{self.file_path}::{'.'.join(scope_stack)}"
|
|
425
427
|
self._edges.append(
|
|
426
|
-
GraphEdge(
|
|
427
|
-
source=self._module_id,
|
|
428
|
-
target=node_id,
|
|
429
|
-
kind=EdgeKind.DEFINES,
|
|
430
|
-
)
|
|
428
|
+
GraphEdge(source=definer, target=node_id, kind=EdgeKind.DEFINES)
|
|
431
429
|
)
|
|
432
430
|
|
|
433
431
|
# Extract base classes from the superclass list and resolve them
|
|
@@ -132,6 +132,13 @@ def get_file_skeleton(
|
|
|
132
132
|
str,
|
|
133
133
|
Field(description="Absolute or relative path to the file"),
|
|
134
134
|
],
|
|
135
|
+
include_imports: Annotated[
|
|
136
|
+
bool,
|
|
137
|
+
Field(
|
|
138
|
+
description="Include import statements in the skeleton. "
|
|
139
|
+
"Set to false to reduce noise when you only need signatures.",
|
|
140
|
+
),
|
|
141
|
+
] = True,
|
|
135
142
|
) -> str:
|
|
136
143
|
"""Get the API surface of a file (signatures, no bodies)."""
|
|
137
144
|
from coderay.skeleton.extractor import extract_skeleton
|
|
@@ -140,7 +147,7 @@ def get_file_skeleton(
|
|
|
140
147
|
if not p.is_file():
|
|
141
148
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
142
149
|
content = p.read_text(encoding="utf-8", errors="replace")
|
|
143
|
-
return extract_skeleton(p, content)
|
|
150
|
+
return extract_skeleton(p, content, include_imports=include_imports)
|
|
144
151
|
|
|
145
152
|
|
|
146
153
|
@mcp.tool(
|
|
@@ -176,10 +183,7 @@ def get_impact_radius(
|
|
|
176
183
|
"No graph found. Ask the user to run 'coderay build' "
|
|
177
184
|
"in their terminal, then retry."
|
|
178
185
|
)
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
"results": [n.to_dict() for n in impact],
|
|
182
|
-
}
|
|
186
|
+
return graph.get_impact_radius(node_id, depth=max_depth).to_dict()
|
|
183
187
|
|
|
184
188
|
|
|
185
189
|
@mcp.resource(
|
|
@@ -14,6 +14,7 @@ class SearchResult:
|
|
|
14
14
|
end_line: int
|
|
15
15
|
symbol: str
|
|
16
16
|
content: str
|
|
17
|
+
score: float = 0.0
|
|
17
18
|
truncated: bool = False
|
|
18
19
|
|
|
19
20
|
@classmethod
|
|
@@ -46,6 +47,7 @@ class SearchResult:
|
|
|
46
47
|
end_line=row["end_line"],
|
|
47
48
|
symbol=row["symbol"],
|
|
48
49
|
content=content,
|
|
50
|
+
score=float(row.get("score", 0.0)),
|
|
49
51
|
truncated=truncated,
|
|
50
52
|
)
|
|
51
53
|
|
|
@@ -57,6 +59,7 @@ class SearchResult:
|
|
|
57
59
|
"end_line": self.end_line,
|
|
58
60
|
"symbol": self.symbol,
|
|
59
61
|
"content": self.content,
|
|
62
|
+
"score": self.score,
|
|
60
63
|
}
|
|
61
64
|
if self.truncated:
|
|
62
65
|
d["truncated"] = True
|
|
@@ -13,13 +13,22 @@ ELLIPSIS = "..."
|
|
|
13
13
|
def extract_skeleton(
|
|
14
14
|
path: str | Path,
|
|
15
15
|
content: str,
|
|
16
|
+
*,
|
|
17
|
+
include_imports: bool = True,
|
|
16
18
|
) -> str:
|
|
17
|
-
"""Extract the skeleton of a source file (signatures, no bodies).
|
|
19
|
+
"""Extract the skeleton of a source file (signatures, no bodies).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
path: Source file path (used for language detection).
|
|
23
|
+
content: Full file content.
|
|
24
|
+
include_imports: When False, import statements are omitted from
|
|
25
|
+
the output to reduce noise.
|
|
26
|
+
"""
|
|
18
27
|
ctx = parse_file(path, content)
|
|
19
28
|
if ctx is None:
|
|
20
29
|
return content
|
|
21
30
|
|
|
22
|
-
parser = SkeletonTreeSitterParser(ctx)
|
|
31
|
+
parser = SkeletonTreeSitterParser(ctx, include_imports=include_imports)
|
|
23
32
|
try:
|
|
24
33
|
lines = parser.collect_lines()
|
|
25
34
|
except Exception: # pragma: no cover - defensive fallback
|
|
@@ -31,6 +40,10 @@ def extract_skeleton(
|
|
|
31
40
|
class SkeletonTreeSitterParser(BaseTreeSitterParser):
|
|
32
41
|
"""Tree-sitter based skeleton extractor for source files."""
|
|
33
42
|
|
|
43
|
+
def __init__(self, context, *, include_imports: bool = True) -> None:
|
|
44
|
+
super().__init__(context)
|
|
45
|
+
self._include_imports = include_imports
|
|
46
|
+
|
|
34
47
|
def collect_lines(self) -> list[str]:
|
|
35
48
|
"""Return the skeleton of a file as a list of lines."""
|
|
36
49
|
tree = self.get_tree()
|
|
@@ -111,7 +124,9 @@ class SkeletonTreeSitterParser(BaseTreeSitterParser):
|
|
|
111
124
|
return
|
|
112
125
|
|
|
113
126
|
if ntype in lang_cfg.import_types:
|
|
114
|
-
|
|
127
|
+
if self._include_imports:
|
|
128
|
+
lines.append(indent + self.node_text(node).strip())
|
|
129
|
+
return
|
|
115
130
|
|
|
116
131
|
if ntype in lang_cfg.decorator_scope_types:
|
|
117
132
|
for child in node.named_children:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|