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.
Files changed (50) hide show
  1. {coderay-1.0.6/src/coderay.egg-info → coderay-1.0.7}/PKG-INFO +1 -1
  2. {coderay-1.0.6 → coderay-1.0.7}/pyproject.toml +1 -1
  3. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/models.py +19 -0
  4. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/code_graph.py +64 -17
  5. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/extractor.py +8 -10
  6. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/mcp_server/server.py +9 -5
  7. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/models.py +3 -0
  8. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/skeleton/extractor.py +18 -3
  9. {coderay-1.0.6 → coderay-1.0.7/src/coderay.egg-info}/PKG-INFO +1 -1
  10. {coderay-1.0.6 → coderay-1.0.7}/LICENSE +0 -0
  11. {coderay-1.0.6 → coderay-1.0.7}/README.md +0 -0
  12. {coderay-1.0.6 → coderay-1.0.7}/setup.cfg +0 -0
  13. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/__init__.py +0 -0
  14. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/chunking/__init__.py +0 -0
  15. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/chunking/chunker.py +0 -0
  16. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/cli/__init__.py +0 -0
  17. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/cli/commands.py +0 -0
  18. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/__init__.py +0 -0
  19. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/config.py +0 -0
  20. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/lock.py +0 -0
  21. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/timing.py +0 -0
  22. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/core/utils.py +0 -0
  23. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/embedding/__init__.py +0 -0
  24. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/embedding/base.py +0 -0
  25. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/embedding/local.py +0 -0
  26. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/__init__.py +0 -0
  27. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/graph/builder.py +0 -0
  28. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/mcp_server/__init__.py +0 -0
  29. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/mcp_server/errors.py +0 -0
  30. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/parsing/base.py +0 -0
  31. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/parsing/languages.py +0 -0
  32. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/pipeline/__init__.py +0 -0
  33. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/pipeline/indexer.py +0 -0
  34. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/pipeline/watcher.py +0 -0
  35. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/__init__.py +0 -0
  36. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/boosting.py +0 -0
  37. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/retrieval/search.py +0 -0
  38. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/skeleton/__init__.py +0 -0
  39. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/state/__init__.py +0 -0
  40. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/state/machine.py +0 -0
  41. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/state/version.py +0 -0
  42. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/storage/__init__.py +0 -0
  43. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/storage/lancedb.py +0 -0
  44. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/vcs/__init__.py +0 -0
  45. {coderay-1.0.6 → coderay-1.0.7}/src/coderay/vcs/git.py +0 -0
  46. {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/SOURCES.txt +0 -0
  47. {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/dependency_links.txt +0 -0
  48. {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/entry_points.txt +0 -0
  49. {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/requires.txt +0 -0
  50. {coderay-1.0.6 → coderay-1.0.7}/src/coderay.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderay
3
- Version: 1.0.6
3
+ Version: 1.0.7
4
4
  Summary: X-ray your codebase — semantic search, code graphs, file skeletons, and MCP server
5
5
  Author-email: Bogdan Copocean <bogdancopocean@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coderay"
7
- version = "1.0.6"
7
+ version = "1.0.7"
8
8
  description = "X-ray your codebase — semantic search, code graphs, file skeletons, and MCP server"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- def get_impact_radius(self, symbol: str, depth: int = 2) -> list[GraphNode]:
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
- depth: Number of reverse-BFS hops. Higher values may return
288
- very large sets.
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
- frontier = {resolved}
295
- for _ in range(depth):
296
- next_frontier: set[str] = set()
297
- for nid in frontier:
298
- if nid not in self._g:
299
- continue
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 pred not in visited:
302
- visited.add(pred)
303
- next_frontier.add(pred)
304
- frontier = next_frontier
305
- return [self.get_node(nid) for nid in visited if self.get_node(nid) is not None]
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
- impact = graph.get_impact_radius(node_id, depth=max_depth)
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
- lines.append(indent + self.node_text(node).strip())
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderay
3
- Version: 1.0.6
3
+ Version: 1.0.7
4
4
  Summary: X-ray your codebase — semantic search, code graphs, file skeletons, and MCP server
5
5
  Author-email: Bogdan Copocean <bogdancopocean@gmail.com>
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
File without changes
File without changes