codespine 0.4.2__tar.gz → 0.4.3__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. {codespine-0.4.2 → codespine-0.4.3}/PKG-INFO +2 -2
  2. {codespine-0.4.2 → codespine-0.4.3}/codespine/__init__.py +1 -1
  3. {codespine-0.4.2 → codespine-0.4.3}/codespine/analysis/deadcode.py +49 -18
  4. {codespine-0.4.2 → codespine-0.4.3}/codespine/mcp/server.py +42 -8
  5. {codespine-0.4.2 → codespine-0.4.3}/codespine.egg-info/PKG-INFO +2 -2
  6. {codespine-0.4.2 → codespine-0.4.3}/codespine.egg-info/requires.txt +1 -1
  7. {codespine-0.4.2 → codespine-0.4.3}/pyproject.toml +2 -2
  8. {codespine-0.4.2 → codespine-0.4.3}/LICENSE +0 -0
  9. {codespine-0.4.2 → codespine-0.4.3}/README.md +0 -0
  10. {codespine-0.4.2 → codespine-0.4.3}/codespine/analysis/__init__.py +0 -0
  11. {codespine-0.4.2 → codespine-0.4.3}/codespine/analysis/community.py +0 -0
  12. {codespine-0.4.2 → codespine-0.4.3}/codespine/analysis/context.py +0 -0
  13. {codespine-0.4.2 → codespine-0.4.3}/codespine/analysis/coupling.py +0 -0
  14. {codespine-0.4.2 → codespine-0.4.3}/codespine/analysis/flow.py +0 -0
  15. {codespine-0.4.2 → codespine-0.4.3}/codespine/analysis/impact.py +0 -0
  16. {codespine-0.4.2 → codespine-0.4.3}/codespine/cli.py +0 -0
  17. {codespine-0.4.2 → codespine-0.4.3}/codespine/config.py +0 -0
  18. {codespine-0.4.2 → codespine-0.4.3}/codespine/db/__init__.py +0 -0
  19. {codespine-0.4.2 → codespine-0.4.3}/codespine/db/schema.py +0 -0
  20. {codespine-0.4.2 → codespine-0.4.3}/codespine/db/store.py +0 -0
  21. {codespine-0.4.2 → codespine-0.4.3}/codespine/diff/__init__.py +0 -0
  22. {codespine-0.4.2 → codespine-0.4.3}/codespine/diff/branch_diff.py +0 -0
  23. {codespine-0.4.2 → codespine-0.4.3}/codespine/indexer/__init__.py +0 -0
  24. {codespine-0.4.2 → codespine-0.4.3}/codespine/indexer/call_resolver.py +0 -0
  25. {codespine-0.4.2 → codespine-0.4.3}/codespine/indexer/engine.py +0 -0
  26. {codespine-0.4.2 → codespine-0.4.3}/codespine/indexer/java_parser.py +0 -0
  27. {codespine-0.4.2 → codespine-0.4.3}/codespine/indexer/symbol_builder.py +0 -0
  28. {codespine-0.4.2 → codespine-0.4.3}/codespine/mcp/__init__.py +0 -0
  29. {codespine-0.4.2 → codespine-0.4.3}/codespine/noise/__init__.py +0 -0
  30. {codespine-0.4.2 → codespine-0.4.3}/codespine/noise/blocklist.py +0 -0
  31. {codespine-0.4.2 → codespine-0.4.3}/codespine/search/__init__.py +0 -0
  32. {codespine-0.4.2 → codespine-0.4.3}/codespine/search/bm25.py +0 -0
  33. {codespine-0.4.2 → codespine-0.4.3}/codespine/search/fuzzy.py +0 -0
  34. {codespine-0.4.2 → codespine-0.4.3}/codespine/search/hybrid.py +0 -0
  35. {codespine-0.4.2 → codespine-0.4.3}/codespine/search/rrf.py +0 -0
  36. {codespine-0.4.2 → codespine-0.4.3}/codespine/search/vector.py +0 -0
  37. {codespine-0.4.2 → codespine-0.4.3}/codespine/watch/__init__.py +0 -0
  38. {codespine-0.4.2 → codespine-0.4.3}/codespine/watch/watcher.py +0 -0
  39. {codespine-0.4.2 → codespine-0.4.3}/codespine.egg-info/SOURCES.txt +0 -0
  40. {codespine-0.4.2 → codespine-0.4.3}/codespine.egg-info/dependency_links.txt +0 -0
  41. {codespine-0.4.2 → codespine-0.4.3}/codespine.egg-info/entry_points.txt +0 -0
  42. {codespine-0.4.2 → codespine-0.4.3}/codespine.egg-info/top_level.txt +0 -0
  43. {codespine-0.4.2 → codespine-0.4.3}/gindex.py +0 -0
  44. {codespine-0.4.2 → codespine-0.4.3}/setup.cfg +0 -0
  45. {codespine-0.4.2 → codespine-0.4.3}/tests/test_branch_diff_normalize.py +0 -0
  46. {codespine-0.4.2 → codespine-0.4.3}/tests/test_call_resolver.py +0 -0
  47. {codespine-0.4.2 → codespine-0.4.3}/tests/test_index_and_hybrid.py +0 -0
  48. {codespine-0.4.2 → codespine-0.4.3}/tests/test_java_parser.py +0 -0
  49. {codespine-0.4.2 → codespine-0.4.3}/tests/test_multimodule_index.py +0 -0
  50. {codespine-0.4.2 → codespine-0.4.3}/tests/test_search_ranking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -46,7 +46,7 @@ Requires-Dist: click
46
46
  Requires-Dist: kuzu
47
47
  Requires-Dist: tree-sitter
48
48
  Requires-Dist: tree-sitter-java
49
- Requires-Dist: fastmcp
49
+ Requires-Dist: fastmcp>=2.3.0
50
50
  Requires-Dist: psutil
51
51
  Requires-Dist: watchfiles
52
52
  Provides-Extra: ml
@@ -1,4 +1,4 @@
1
1
  """CodeSpine package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.4.2"
4
+ __version__ = "0.4.3"
@@ -74,8 +74,17 @@ def _modifier_tokens(modifiers) -> set[str]:
74
74
  return {str(m).strip() for m in modifiers}
75
75
 
76
76
 
77
- def detect_dead_code(store, limit: int = 200, project: str | None = None) -> list[dict]:
78
- """Java-aware dead code detection with exemption passes."""
77
+ def detect_dead_code(store, limit: int = 200, project: str | None = None) -> list[dict] | None:
78
+ """Java-aware dead code detection with exemption passes.
79
+
80
+ Returns a list of dead method dicts, each with:
81
+ method_id, name, signature, class_fqcn, file_path, reason.
82
+
83
+ The return value is augmented with a ``_stats`` entry (a sentinel dict
84
+ with key ``_stats``) containing pre/post-exemption counts so callers can
85
+ show users that the exemption logic is actually working:
86
+ candidates_with_no_callers, exempted, dead_returned
87
+ """
79
88
  if project:
80
89
  candidates = store.query_records(
81
90
  """
@@ -88,16 +97,17 @@ def detect_dead_code(store, limit: int = 200, project: str | None = None) -> lis
88
97
  m.modifiers as modifiers,
89
98
  c.fqcn as class_fqcn,
90
99
  m.is_constructor as is_constructor,
91
- m.is_test as is_test
100
+ m.is_test as is_test,
101
+ f.path as file_path
92
102
  LIMIT $limit
93
103
  """,
94
- {"limit": int(limit * 3), "proj": project},
104
+ {"limit": int(limit * 5), "proj": project},
95
105
  )
96
106
  else:
97
107
  candidates = store.query_records(
98
108
  """
99
- MATCH (m:Method), (c:Class)
100
- WHERE m.class_id = c.id
109
+ MATCH (m:Method), (c:Class), (f:File)
110
+ WHERE m.class_id = c.id AND c.file_id = f.id
101
111
  AND NOT EXISTS { MATCH (:Method)-[:CALLS]->(m) }
102
112
  RETURN m.id as method_id,
103
113
  m.name as name,
@@ -105,15 +115,17 @@ def detect_dead_code(store, limit: int = 200, project: str | None = None) -> lis
105
115
  m.modifiers as modifiers,
106
116
  c.fqcn as class_fqcn,
107
117
  m.is_constructor as is_constructor,
108
- m.is_test as is_test
118
+ m.is_test as is_test,
119
+ f.path as file_path
109
120
  LIMIT $limit
110
121
  """,
111
- {"limit": int(limit * 3)},
122
+ {"limit": int(limit * 5)},
112
123
  )
113
124
 
114
125
  if not candidates:
115
126
  return []
116
127
 
128
+ n_candidates = len(candidates)
117
129
  exempt: set[str] = set()
118
130
 
119
131
  # Exempt constructors, test methods, and Java main entrypoints.
@@ -138,22 +150,19 @@ def detect_dead_code(store, limit: int = 200, project: str | None = None) -> lis
138
150
  if name in {"valueOf", "fromString", "builder"}:
139
151
  exempt.add(c["method_id"])
140
152
 
141
- # Exempt override/interface contract methods if relation exists.
153
+ # Exempt methods that DIRECTLY override another method (precise: only the
154
+ # specific overriding method is exempted, not the entire implementing class).
155
+ # NOTE: we intentionally do NOT use the class-level IMPLEMENTS relation here
156
+ # because that would exempt ALL methods of every class that implements ANY
157
+ # interface — in a typical Spring project that wipes out almost everything
158
+ # and produces 0 dead code results.
142
159
  override_methods = store.query_records(
143
160
  """
144
161
  MATCH (m:Method)-[:OVERRIDES]->(:Method)
145
162
  RETURN DISTINCT m.id as method_id
146
163
  """
147
164
  )
148
- interface_methods = store.query_records(
149
- """
150
- MATCH (c:Class)-[:IMPLEMENTS]->(:Class), (m:Method)
151
- WHERE m.class_id = c.id
152
- RETURN DISTINCT m.id as method_id
153
- """
154
- )
155
165
  exempt.update(r["method_id"] for r in override_methods)
156
- exempt.update(r["method_id"] for r in interface_methods)
157
166
 
158
167
  dead = []
159
168
  for c in candidates:
@@ -164,8 +173,30 @@ def detect_dead_code(store, limit: int = 200, project: str | None = None) -> lis
164
173
  "method_id": c["method_id"],
165
174
  "name": c.get("name"),
166
175
  "signature": c.get("signature"),
176
+ "class_fqcn": c.get("class_fqcn"),
177
+ "file_path": c.get("file_path"),
167
178
  "reason": "no_incoming_calls_after_exemptions",
168
179
  }
169
180
  )
170
181
 
171
- return dead[:limit]
182
+ result = dead[:limit]
183
+
184
+ # Append stats as a sentinel entry so the MCP layer can surface them
185
+ # without changing the return type. Callers should strip entries that
186
+ # have a "_stats" key when iterating over method results.
187
+ result.append({
188
+ "_stats": {
189
+ "candidates_with_no_callers": n_candidates,
190
+ "exempted": len(exempt),
191
+ "dead_returned": len(result),
192
+ "note": (
193
+ "Exemptions cover: constructors, test methods, main(), "
194
+ "toString/hashCode/equals/compareTo, public getters/setters, "
195
+ "methods with DI/framework annotations, and direct method overrides. "
196
+ "The class-level IMPLEMENTS exemption has been removed — only "
197
+ "methods with direct OVERRIDES relations are now exempted."
198
+ ),
199
+ }
200
+ })
201
+
202
+ return result
@@ -253,11 +253,31 @@ def build_mcp_server(store, repo_path_provider):
253
253
  """
254
254
  Detect methods with no incoming calls (after Java-aware exemptions).
255
255
  Pass project to scope to a single module.
256
+
257
+ Returns dead_code list, count, and an exemption_stats dict showing
258
+ how many candidates were found and how many were filtered out by the
259
+ exemption rules — useful for validating that the feature is working
260
+ even when the dead list is empty.
256
261
  """
257
- dead = detect_dead_code_analysis(store, limit=limit, project=project)
258
- if dead is None:
262
+ raw = detect_dead_code_analysis(store, limit=limit, project=project)
263
+ if raw is None:
259
264
  return _no_symbols_response()
260
- return {"available": True, "dead_code": dead, "count": len(dead)}
265
+
266
+ # Separate the sentinel stats entry appended by the analysis function.
267
+ stats: dict = {}
268
+ dead = []
269
+ for entry in raw:
270
+ if "_stats" in entry:
271
+ stats = entry["_stats"]
272
+ else:
273
+ dead.append(entry)
274
+
275
+ return {
276
+ "available": True,
277
+ "dead_code": dead,
278
+ "count": len(dead),
279
+ "exemption_stats": stats,
280
+ }
261
281
 
262
282
  @mcp.tool()
263
283
  def trace_execution_flows(entry_symbol: str | None = None, max_depth: int = 6, project: str | None = None):
@@ -589,11 +609,24 @@ def build_mcp_server(store, repo_path_provider):
589
609
  }
590
610
 
591
611
  @mcp.tool()
592
- def compare_branches(base_ref: str, head_ref: str):
593
- """Symbol-level diff between two git refs (branches, tags, commits)."""
594
- repo = repo_path_provider()
612
+ def compare_branches(base_ref: str, head_ref: str, project: str | None = None):
613
+ """
614
+ Symbol-level diff between two git refs (branches, tags, commits).
615
+ Pass project=<project_id> so the tool can resolve the correct git
616
+ repository root from the indexed project path rather than relying on
617
+ the MCP server's working directory (which may point to the graph DB
618
+ location, not the source tree).
619
+ """
620
+ repo = _resolve_repo_path(store, project, repo_path_provider)
595
621
  if not _git_available(repo):
596
- return {"available": False, "note": "Not a git repository (or git not installed)."}
622
+ return {
623
+ "available": False,
624
+ "note": (
625
+ "Not a git repository (or git not installed). "
626
+ "Pass project=<project_id> so the tool can resolve the repo "
627
+ "from the indexed project path. Use list_projects() to see available IDs."
628
+ ),
629
+ }
597
630
  result = compare_branches_analysis(repo, base_ref, head_ref)
598
631
  return {"available": True, **result}
599
632
 
@@ -981,6 +1014,7 @@ def build_mcp_server(store, repo_path_provider):
981
1014
  @mcp.tool()
982
1015
  def run_cypher(query: str):
983
1016
  """Run a raw Cypher query against the graph. For advanced exploration."""
984
- return store.query_records(query)
1017
+ records = store.query_records(query)
1018
+ return {"available": True, "records": records, "count": len(records)}
985
1019
 
986
1020
  return mcp
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -46,7 +46,7 @@ Requires-Dist: click
46
46
  Requires-Dist: kuzu
47
47
  Requires-Dist: tree-sitter
48
48
  Requires-Dist: tree-sitter-java
49
- Requires-Dist: fastmcp
49
+ Requires-Dist: fastmcp>=2.3.0
50
50
  Requires-Dist: psutil
51
51
  Requires-Dist: watchfiles
52
52
  Provides-Extra: ml
@@ -2,7 +2,7 @@ click
2
2
  kuzu
3
3
  tree-sitter
4
4
  tree-sitter-java
5
- fastmcp
5
+ fastmcp>=2.3.0
6
6
  psutil
7
7
  watchfiles
8
8
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codespine"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Local Java code intelligence indexer backed by a graph database"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "kuzu",
31
31
  "tree-sitter",
32
32
  "tree-sitter-java",
33
- "fastmcp",
33
+ "fastmcp>=2.3.0",
34
34
  "psutil",
35
35
  "watchfiles"
36
36
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes