codespine 0.7.2__tar.gz → 0.8.0__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.
- {codespine-0.7.2 → codespine-0.8.0}/PKG-INFO +2 -2
- {codespine-0.7.2 → codespine-0.8.0}/README.md +1 -1
- {codespine-0.7.2 → codespine-0.8.0}/codespine/__init__.py +1 -1
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/crossmodule.py +26 -28
- {codespine-0.7.2 → codespine-0.8.0}/codespine/db/schema.py +6 -1
- {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/call_resolver.py +7 -3
- {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/engine.py +29 -1
- {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/java_parser.py +23 -4
- {codespine-0.7.2 → codespine-0.8.0}/codespine/mcp/server.py +88 -11
- codespine-0.8.0/codespine/noise/blocklist.py +33 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/watch/watcher.py +6 -3
- {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/PKG-INFO +2 -2
- {codespine-0.7.2 → codespine-0.8.0}/pyproject.toml +1 -1
- codespine-0.7.2/codespine/noise/blocklist.py +0 -37
- {codespine-0.7.2 → codespine-0.8.0}/LICENSE +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/community.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/context.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/coupling.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/deadcode.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/flow.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/impact.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/cli.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/config.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/db/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/db/store.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/diff/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/diff/branch_diff.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/guide.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/symbol_builder.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/mcp/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/noise/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/git_state.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/merge.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/store.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/search/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/search/bm25.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/search/fuzzy.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/search/hybrid.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/search/rrf.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/search/vector.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine/watch/__init__.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/SOURCES.txt +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/dependency_links.txt +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/entry_points.txt +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/requires.txt +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/top_level.txt +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/gindex.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/setup.cfg +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_branch_diff_normalize.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_call_resolver.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_community_detection.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_deadcode.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_index_and_hybrid.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_java_parser.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_multimodule_index.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_overlay.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_search_ranking.py +0 -0
- {codespine-0.7.2 → codespine-0.8.0}/tests/test_store_recovery.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codespine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Local Java code intelligence indexer backed by a graph database
|
|
5
5
|
Author: CodeSpine contributors
|
|
6
6
|
License: MIT License
|
|
@@ -267,7 +267,7 @@ codespine guide --json # structured JSON for tooling
|
|
|
267
267
|
| `detect_dead_code(limit, project, strict)` | Methods with no callers (Java-aware exemptions). |
|
|
268
268
|
| `trace_execution_flows(entry_symbol, max_depth, project)` | Execution paths from entry points. |
|
|
269
269
|
| `get_symbol_community(symbol)` | Architectural community cluster for a symbol. |
|
|
270
|
-
| `get_change_coupling(
|
|
270
|
+
| `get_change_coupling(days, min_strength, min_cochanges)` | Files that changed together in the last N days (default 5). |
|
|
271
271
|
|
|
272
272
|
**Git**
|
|
273
273
|
|
|
@@ -203,7 +203,7 @@ codespine guide --json # structured JSON for tooling
|
|
|
203
203
|
| `detect_dead_code(limit, project, strict)` | Methods with no callers (Java-aware exemptions). |
|
|
204
204
|
| `trace_execution_flows(entry_symbol, max_depth, project)` | Execution paths from entry points. |
|
|
205
205
|
| `get_symbol_community(symbol)` | Architectural community cluster for a symbol. |
|
|
206
|
-
| `get_change_coupling(
|
|
206
|
+
| `get_change_coupling(days, min_strength, min_cochanges)` | Files that changed together in the last N days (default 5). |
|
|
207
207
|
|
|
208
208
|
**Git**
|
|
209
209
|
|
|
@@ -17,11 +17,10 @@ Two linking strategies are applied:
|
|
|
17
17
|
parameter count as a method M_dst in the referenced class. This catches
|
|
18
18
|
delegation, interface-implementation forwarding, and adapter patterns.
|
|
19
19
|
|
|
20
|
-
Strategy B —
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
cross-module from appearing as dead code.
|
|
20
|
+
Strategy B — Direct parameter/return type reference (confidence 0.6)
|
|
21
|
+
When the referenced class name appears directly as a parameter type or
|
|
22
|
+
return type of the source method, create an edge to the class's
|
|
23
|
+
constructor (if any). This catches model/DTO/context instantiation.
|
|
25
24
|
"""
|
|
26
25
|
from __future__ import annotations
|
|
27
26
|
|
|
@@ -165,29 +164,28 @@ def link_cross_module_calls(store, project_ids: list[str] | None = None, progres
|
|
|
165
164
|
LOGGER.debug("Name-match edge failed: %s", exc)
|
|
166
165
|
matched_dst_mids.add(dm["mid"])
|
|
167
166
|
|
|
168
|
-
# Strategy B:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
LOGGER.debug("Fallback edge failed: %s", exc)
|
|
167
|
+
# Strategy B: if the referenced class name appears directly
|
|
168
|
+
# in the source method's parameter types or return type,
|
|
169
|
+
# link to the class's constructor (model/DTO instantiation).
|
|
170
|
+
if not matched_dst_mids:
|
|
171
|
+
rtype_tokens = set(_TOKEN_RE.findall(rtype))
|
|
172
|
+
sig_tokens = set(_TOKEN_RE.findall(sig))
|
|
173
|
+
if class_name in rtype_tokens or class_name in sig_tokens:
|
|
174
|
+
for dm in dst_methods:
|
|
175
|
+
if not dm.get("is_ctor"):
|
|
176
|
+
continue
|
|
177
|
+
pair = (sm["mid"], dm["mid"])
|
|
178
|
+
if pair in seen:
|
|
179
|
+
continue
|
|
180
|
+
seen.add(pair)
|
|
181
|
+
try:
|
|
182
|
+
store.add_call(
|
|
183
|
+
sm["mid"], dm["mid"],
|
|
184
|
+
0.6, "cross_module_ctor_ref",
|
|
185
|
+
)
|
|
186
|
+
new_edges += 1
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
LOGGER.debug("Ctor-ref edge failed: %s", exc)
|
|
191
189
|
|
|
192
190
|
_ping(f"{new_edges} edges created")
|
|
193
191
|
LOGGER.info("Cross-module linking: created %d new call edges.", new_edges)
|
|
@@ -49,7 +49,7 @@ REL_TABLES: Iterable[tuple[str, str]] = [
|
|
|
49
49
|
("IN_FLOW", "CREATE REL TABLE IN_FLOW(FROM Symbol TO Flow, depth INT64)"),
|
|
50
50
|
(
|
|
51
51
|
"CO_CHANGED_WITH",
|
|
52
|
-
"CREATE REL TABLE CO_CHANGED_WITH(FROM File TO File, strength DOUBLE, cochanges INT64,
|
|
52
|
+
"CREATE REL TABLE CO_CHANGED_WITH(FROM File TO File, strength DOUBLE, cochanges INT64, days INT64)",
|
|
53
53
|
),
|
|
54
54
|
]
|
|
55
55
|
|
|
@@ -86,3 +86,8 @@ def ensure_schema(conn) -> None:
|
|
|
86
86
|
|
|
87
87
|
_safe_execute(conn, "ALTER TABLE Project ADD indexed_commit STRING DEFAULT ''")
|
|
88
88
|
_safe_execute(conn, "ALTER TABLE Project ADD overlay_dirty BOOL DEFAULT false")
|
|
89
|
+
|
|
90
|
+
# v0.7.3: renamed CO_CHANGED_WITH.months → days (days-based window).
|
|
91
|
+
# ALTER TABLE is a no-op on fresh DBs that already have 'days'; safe_execute
|
|
92
|
+
# swallows the error if the column already exists or the table doesn't yet.
|
|
93
|
+
_safe_execute(conn, "ALTER TABLE CO_CHANGED_WITH ADD days INT64 DEFAULT 0")
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from typing import Iterator
|
|
5
5
|
|
|
6
|
-
from codespine.noise.blocklist import NOISE_METHOD_NAMES
|
|
6
|
+
from codespine.noise.blocklist import MIN_FUZZY_NAME_LEN, NOISE_METHOD_NAMES
|
|
7
7
|
|
|
8
8
|
MAX_FUZZY_TARGETS = 12
|
|
9
9
|
|
|
@@ -84,8 +84,6 @@ def resolve_calls(
|
|
|
84
84
|
|
|
85
85
|
for call in call_sites:
|
|
86
86
|
call_name = call.name
|
|
87
|
-
if call_name in NOISE_METHOD_NAMES:
|
|
88
|
-
continue
|
|
89
87
|
|
|
90
88
|
key = (call_name, int(call.arg_count))
|
|
91
89
|
targets: list[str] = []
|
|
@@ -123,6 +121,12 @@ def resolve_calls(
|
|
|
123
121
|
reason = "intra_class_exact"
|
|
124
122
|
|
|
125
123
|
if not targets:
|
|
124
|
+
# Skip noise method names and short names in the fuzzy global
|
|
125
|
+
# fallback — they are too ambiguous without receiver context.
|
|
126
|
+
if call_name in NOISE_METHOD_NAMES:
|
|
127
|
+
continue
|
|
128
|
+
if len(call_name) < MIN_FUZZY_NAME_LEN:
|
|
129
|
+
continue
|
|
126
130
|
# Prefer same-package candidates before global fallback.
|
|
127
131
|
src_pkg = src_ctx.get("package", "")
|
|
128
132
|
same_pkg = []
|
|
@@ -153,7 +153,20 @@ class JavaIndexer:
|
|
|
153
153
|
) -> IndexResult:
|
|
154
154
|
root_path = os.path.abspath(root_path)
|
|
155
155
|
if project_id is None:
|
|
156
|
-
|
|
156
|
+
# Reuse the existing project ID for this path if one exists.
|
|
157
|
+
# This prevents ID drift when the same module is re-indexed
|
|
158
|
+
# directly (vision-server) vs from a workspace root (vision::vision-server).
|
|
159
|
+
try:
|
|
160
|
+
existing = self.store.query_records(
|
|
161
|
+
"MATCH (p:Project) WHERE p.path = $path RETURN p.id as id LIMIT 1",
|
|
162
|
+
{"path": root_path},
|
|
163
|
+
)
|
|
164
|
+
if existing:
|
|
165
|
+
project_id = existing[0]["id"]
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
if project_id is None:
|
|
169
|
+
project_id = os.path.basename(root_path)
|
|
157
170
|
current_files = self._collect_java_files(root_path)
|
|
158
171
|
self._emit(progress, "scan_done", files_found=len(current_files))
|
|
159
172
|
db_files = self.store.project_file_hashes(project_id) if not full else {}
|
|
@@ -337,6 +350,21 @@ class JavaIndexer:
|
|
|
337
350
|
)
|
|
338
351
|
classes_indexed += 1
|
|
339
352
|
|
|
353
|
+
for fld in cls.fields:
|
|
354
|
+
fqfield = f"{cls.fqcn}#{fld.name}"
|
|
355
|
+
symbol_rows.append(
|
|
356
|
+
{
|
|
357
|
+
"id": symbol_id("field", fqfield, scope),
|
|
358
|
+
"kind": "field",
|
|
359
|
+
"name": fld.name,
|
|
360
|
+
"fqname": fqfield,
|
|
361
|
+
"file_id": f_id,
|
|
362
|
+
"line": fld.line,
|
|
363
|
+
"col": fld.col,
|
|
364
|
+
"embedding": embed_text(f"field {fqfield} {fld.type_name}") if embed else None,
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
340
368
|
for method in cls.methods:
|
|
341
369
|
m_id = method_id(cls.fqcn, method.signature, scope)
|
|
342
370
|
method_rows.append(
|
|
@@ -35,6 +35,14 @@ class ParsedCall:
|
|
|
35
35
|
col: int
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
@dataclass
|
|
39
|
+
class ParsedField:
|
|
40
|
+
name: str
|
|
41
|
+
type_name: str
|
|
42
|
+
line: int
|
|
43
|
+
col: int
|
|
44
|
+
|
|
45
|
+
|
|
38
46
|
@dataclass
|
|
39
47
|
class ParsedClass:
|
|
40
48
|
name: str
|
|
@@ -49,6 +57,7 @@ class ParsedClass:
|
|
|
49
57
|
field_types: dict[str, str] = field(default_factory=dict)
|
|
50
58
|
body_hash: str = ""
|
|
51
59
|
methods: list[ParsedMethod] = field(default_factory=list)
|
|
60
|
+
fields: list[ParsedField] = field(default_factory=list)
|
|
52
61
|
|
|
53
62
|
|
|
54
63
|
@dataclass
|
|
@@ -193,7 +202,7 @@ def _extract_local_types(method_node) -> dict[str, str]:
|
|
|
193
202
|
return locals_map
|
|
194
203
|
|
|
195
204
|
|
|
196
|
-
def _extract_field_types(class_node) -> dict[str, str]:
|
|
205
|
+
def _extract_field_types(class_node) -> tuple[dict[str, str], list[ParsedField]]:
|
|
197
206
|
q = Query(
|
|
198
207
|
JAVA_LANGUAGE,
|
|
199
208
|
"""
|
|
@@ -204,13 +213,21 @@ def _extract_field_types(class_node) -> dict[str, str]:
|
|
|
204
213
|
)
|
|
205
214
|
captures = _captures(q, class_node)
|
|
206
215
|
field_map: dict[str, str] = {}
|
|
216
|
+
field_list: list[ParsedField] = []
|
|
207
217
|
current_type = None
|
|
208
218
|
for node, tag in captures:
|
|
209
219
|
if tag == "type":
|
|
210
220
|
current_type = _node_type_name(node)
|
|
211
221
|
elif tag == "name" and current_type:
|
|
212
|
-
|
|
213
|
-
|
|
222
|
+
name = _text(node)
|
|
223
|
+
field_map[name] = current_type
|
|
224
|
+
field_list.append(ParsedField(
|
|
225
|
+
name=name,
|
|
226
|
+
type_name=current_type,
|
|
227
|
+
line=node.start_point[0] + 1,
|
|
228
|
+
col=node.start_point[1] + 1,
|
|
229
|
+
))
|
|
230
|
+
return field_map, field_list
|
|
214
231
|
|
|
215
232
|
|
|
216
233
|
def _extract_parameter_types(params_node) -> list[str]:
|
|
@@ -338,6 +355,7 @@ def parse_java_source(source: bytes) -> ParsedFile:
|
|
|
338
355
|
fqcn = f"{package_name}.{cls_name}" if package_name else cls_name
|
|
339
356
|
cls_modifiers, cls_annotations = _extract_modifiers_and_annotations(node)
|
|
340
357
|
extends_name, interface_names = _extract_inheritance(node)
|
|
358
|
+
ft_map, ft_list = _extract_field_types(node)
|
|
341
359
|
parsed_class = ParsedClass(
|
|
342
360
|
name=cls_name,
|
|
343
361
|
package=package_name,
|
|
@@ -348,8 +366,9 @@ def parse_java_source(source: bytes) -> ParsedFile:
|
|
|
348
366
|
annotations=cls_annotations,
|
|
349
367
|
extends=extends_name,
|
|
350
368
|
interfaces=interface_names,
|
|
351
|
-
field_types=
|
|
369
|
+
field_types=ft_map,
|
|
352
370
|
body_hash=_hash_node(node),
|
|
371
|
+
fields=ft_list,
|
|
353
372
|
)
|
|
354
373
|
|
|
355
374
|
method_nodes = [n for n, t in _captures(method_query, node) if t == "method_decl"]
|
|
@@ -75,6 +75,21 @@ def _no_symbols_response(note: str = "No symbols indexed. Run 'codespine analyse
|
|
|
75
75
|
return _json({"available": False, "note": note})
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _normalize_symbol_input(raw: str) -> str:
|
|
79
|
+
"""Normalize a symbol string so that various user input formats work.
|
|
80
|
+
|
|
81
|
+
Handles:
|
|
82
|
+
- ``com.example.MyClass#myMethod(int,String)`` → ``myMethod(int,String)``
|
|
83
|
+
- ``MyClass#myMethod`` → ``myMethod``
|
|
84
|
+
- ``myMethod(int,String)`` → unchanged
|
|
85
|
+
- ``myMethod`` → unchanged
|
|
86
|
+
"""
|
|
87
|
+
s = raw.strip()
|
|
88
|
+
if "#" in s:
|
|
89
|
+
s = s[s.index("#") + 1:]
|
|
90
|
+
return s
|
|
91
|
+
|
|
92
|
+
|
|
78
93
|
def _parse_indexed_at(raw) -> int:
|
|
79
94
|
"""Robustly parse an indexed_at value that may be str, int, float, or None."""
|
|
80
95
|
if raw is None:
|
|
@@ -256,8 +271,18 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
256
271
|
from codespine.search.vector import _load_model
|
|
257
272
|
has_embeddings = _load_model() is not None
|
|
258
273
|
|
|
274
|
+
# Check git availability on the default path AND on each indexed
|
|
275
|
+
# project path. Project-scoped git operations (git_log, git_diff,
|
|
276
|
+
# compare_branches) work when the project path is a git repo, even
|
|
277
|
+
# if the default path (cwd) is not.
|
|
259
278
|
repo = repo_path_provider()
|
|
260
279
|
git_ok = _git_available(repo)
|
|
280
|
+
if not git_ok:
|
|
281
|
+
for p in projects:
|
|
282
|
+
pp = p.get("path", "")
|
|
283
|
+
if pp and os.path.isdir(pp) and _git_available(pp):
|
|
284
|
+
git_ok = True
|
|
285
|
+
break
|
|
261
286
|
|
|
262
287
|
n_sym = sym_q[0]["count"] if sym_q else 0
|
|
263
288
|
n_comm = comm_q[0]["count"] if comm_q else 0
|
|
@@ -450,7 +475,11 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
450
475
|
Caller-tree impact analysis for a symbol.
|
|
451
476
|
project scopes the target symbol lookup; cross-project callers are always included.
|
|
452
477
|
"""
|
|
453
|
-
|
|
478
|
+
normalized = _normalize_symbol_input(symbol)
|
|
479
|
+
result = analyze_impact(store, normalized, max_depth=max_depth, project=project)
|
|
480
|
+
if not result.get("targets_resolved"):
|
|
481
|
+
# Retry with the raw input in case the ID matched exactly.
|
|
482
|
+
result = analyze_impact(store, symbol, max_depth=max_depth, project=project)
|
|
454
483
|
if not result.get("targets_resolved"):
|
|
455
484
|
return {"available": False, "note": f"Symbol '{symbol}' not found in the index."}
|
|
456
485
|
return _staleness_meta(store, {"available": True, **result}, project, overlay_store=overlay_store)
|
|
@@ -502,6 +531,8 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
502
531
|
Trace execution flows from entry points (main methods, tests).
|
|
503
532
|
Pass project to scope entry-point discovery to a single module.
|
|
504
533
|
"""
|
|
534
|
+
if entry_symbol:
|
|
535
|
+
entry_symbol = _normalize_symbol_input(entry_symbol)
|
|
505
536
|
flows = trace_flows_analysis(store, entry_symbol=entry_symbol, max_depth=max_depth, project=project)
|
|
506
537
|
if not flows:
|
|
507
538
|
return _no_symbols_response("No entry points found. Run 'codespine analyse --deep' or provide entry_symbol.")
|
|
@@ -514,7 +545,10 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
514
545
|
# graph DB read-only, so any write attempt raises "Cannot execute write
|
|
515
546
|
# operations in a read-only database!". Communities are computed once
|
|
516
547
|
# during 'codespine analyse --deep' and persisted; we just read them.
|
|
517
|
-
|
|
548
|
+
normalized = _normalize_symbol_input(symbol)
|
|
549
|
+
result = symbol_community(store, normalized)
|
|
550
|
+
if not result.get("matches"):
|
|
551
|
+
result = symbol_community(store, symbol)
|
|
518
552
|
if not result.get("matches"):
|
|
519
553
|
return {"available": False, "note": "No community data yet. Run 'codespine analyse --deep'."}
|
|
520
554
|
return _staleness_meta(store, {"available": True, **result}, overlay_store=overlay_store, deep_scope=True)
|
|
@@ -646,7 +680,7 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
646
680
|
Parameters:
|
|
647
681
|
name – Simple class/method name, fully-qualified name, or prefix.
|
|
648
682
|
Matching is case-insensitive on the simple name; exact on the FQCN.
|
|
649
|
-
kind – Optional filter: "class" or "
|
|
683
|
+
kind – Optional filter: "class", "method", or "field".
|
|
650
684
|
project – Optional project_id to restrict the search.
|
|
651
685
|
limit – Max results per kind (default 50).
|
|
652
686
|
|
|
@@ -703,7 +737,39 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
703
737
|
if len(methods) >= limit:
|
|
704
738
|
break
|
|
705
739
|
|
|
706
|
-
|
|
740
|
+
fields: list[dict] = []
|
|
741
|
+
if kind in (None, "field"):
|
|
742
|
+
project_clause_f = "AND f.project_id = $proj" if project else ""
|
|
743
|
+
field_params: dict = {"namel": name_lower, "lim": limit}
|
|
744
|
+
if project:
|
|
745
|
+
field_params["proj"] = project
|
|
746
|
+
field_recs = store.query_records(
|
|
747
|
+
f"""
|
|
748
|
+
MATCH (s:Symbol), (f:File)
|
|
749
|
+
WHERE s.file_id = f.id AND s.kind = 'field'
|
|
750
|
+
AND (lower(s.name) = $namel OR lower(s.fqname) CONTAINS $namel)
|
|
751
|
+
{project_clause_f}
|
|
752
|
+
RETURN s.id as id, s.name as name, s.fqname as fqname,
|
|
753
|
+
f.project_id as project_id, f.path as file_path,
|
|
754
|
+
s.line as line, s.col as col
|
|
755
|
+
LIMIT $lim
|
|
756
|
+
""",
|
|
757
|
+
field_params,
|
|
758
|
+
)
|
|
759
|
+
for rec in field_recs:
|
|
760
|
+
fields.append(
|
|
761
|
+
{
|
|
762
|
+
"id": rec.get("id"),
|
|
763
|
+
"name": rec.get("name"),
|
|
764
|
+
"fqname": rec.get("fqname"),
|
|
765
|
+
"project_id": rec.get("project_id"),
|
|
766
|
+
"file_path": rec.get("file_path"),
|
|
767
|
+
"line": rec.get("line"),
|
|
768
|
+
"col": rec.get("col"),
|
|
769
|
+
}
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
total = len(classes) + len(methods) + len(fields)
|
|
707
773
|
if total == 0:
|
|
708
774
|
return {
|
|
709
775
|
"available": False,
|
|
@@ -714,12 +780,16 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
714
780
|
by_project: dict[str, dict] = {}
|
|
715
781
|
for c in classes:
|
|
716
782
|
pid = c.get("project_id", "?")
|
|
717
|
-
by_project.setdefault(pid, {"classes": [], "methods": []})
|
|
783
|
+
by_project.setdefault(pid, {"classes": [], "methods": [], "fields": []})
|
|
718
784
|
by_project[pid]["classes"].append(c)
|
|
719
785
|
for m in methods:
|
|
720
786
|
pid = m.get("project_id", "?")
|
|
721
|
-
by_project.setdefault(pid, {"classes": [], "methods": []})
|
|
787
|
+
by_project.setdefault(pid, {"classes": [], "methods": [], "fields": []})
|
|
722
788
|
by_project[pid]["methods"].append(m)
|
|
789
|
+
for f in fields:
|
|
790
|
+
pid = f.get("project_id", "?")
|
|
791
|
+
by_project.setdefault(pid, {"classes": [], "methods": [], "fields": []})
|
|
792
|
+
by_project[pid]["fields"].append(f)
|
|
723
793
|
|
|
724
794
|
return _staleness_meta(store, {
|
|
725
795
|
"available": True,
|
|
@@ -1367,17 +1437,22 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
1367
1437
|
"""
|
|
1368
1438
|
from codespine.analysis.impact import _resolve_method_metadata
|
|
1369
1439
|
|
|
1440
|
+
# Normalize FQN inputs: "Class#method(sig)" → "method(sig)"
|
|
1441
|
+
normalized = _normalize_symbol_input(symbol)
|
|
1442
|
+
|
|
1370
1443
|
project_clause = "AND f.project_id = $proj" if project else ""
|
|
1371
|
-
params: dict = {"q": symbol}
|
|
1444
|
+
params: dict = {"q": normalized, "raw": symbol}
|
|
1372
1445
|
if project:
|
|
1373
1446
|
params["proj"] = project
|
|
1374
1447
|
|
|
1375
|
-
# 1. Resolve the symbol to method IDs
|
|
1448
|
+
# 1. Resolve the symbol to method IDs. Try both the normalized
|
|
1449
|
+
# form and the raw input so exact-ID matches still work.
|
|
1376
1450
|
method_recs = store.query_records(
|
|
1377
1451
|
f"""
|
|
1378
1452
|
MATCH (m:Method), (c:Class), (f:File)
|
|
1379
1453
|
WHERE m.class_id = c.id AND c.file_id = f.id {project_clause}
|
|
1380
|
-
AND (m.id = $q OR
|
|
1454
|
+
AND (m.id = $q OR m.id = $raw
|
|
1455
|
+
OR lower(m.name) = lower($q)
|
|
1381
1456
|
OR lower(m.signature) CONTAINS lower($q))
|
|
1382
1457
|
RETURN m.id as id, m.name as name, m.signature as signature,
|
|
1383
1458
|
c.id as class_id, c.fqcn as class_fqcn,
|
|
@@ -1393,20 +1468,22 @@ def build_mcp_server(store, repo_path_provider):
|
|
|
1393
1468
|
mid = target["id"]
|
|
1394
1469
|
cid = target["class_id"]
|
|
1395
1470
|
|
|
1396
|
-
# 2. Callers (upstream)
|
|
1471
|
+
# 2. Callers (upstream) — exclude low-confidence cross-module fallback edges
|
|
1397
1472
|
callers = store.query_records(
|
|
1398
1473
|
"""
|
|
1399
1474
|
MATCH (caller:Method)-[r:CALLS]->(m:Method {id: $mid})
|
|
1475
|
+
WHERE coalesce(r.confidence, 0.5) >= 0.5
|
|
1400
1476
|
RETURN caller.id as id, coalesce(r.confidence, 0.5) as confidence,
|
|
1401
1477
|
coalesce(r.reason, 'unknown') as reason
|
|
1402
1478
|
""",
|
|
1403
1479
|
{"mid": mid},
|
|
1404
1480
|
)
|
|
1405
1481
|
|
|
1406
|
-
# 3. Callees (downstream)
|
|
1482
|
+
# 3. Callees (downstream) — exclude low-confidence cross-module fallback edges
|
|
1407
1483
|
callees = store.query_records(
|
|
1408
1484
|
"""
|
|
1409
1485
|
MATCH (m:Method {id: $mid})-[r:CALLS]->(callee:Method)
|
|
1486
|
+
WHERE coalesce(r.confidence, 0.5) >= 0.5
|
|
1410
1487
|
RETURN callee.id as id, coalesce(r.confidence, 0.5) as confidence,
|
|
1411
1488
|
coalesce(r.reason, 'unknown') as reason
|
|
1412
1489
|
""",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Noise filters for call graph generation."""
|
|
2
|
+
|
|
3
|
+
NOISE_METHOD_NAMES = {
|
|
4
|
+
# Object / lang
|
|
5
|
+
"print", "println", "printf",
|
|
6
|
+
"hashCode", "equals", "toString", "getClass",
|
|
7
|
+
"notify", "notifyAll", "wait", "clone", "finalize",
|
|
8
|
+
"compareTo",
|
|
9
|
+
# Collections / streams
|
|
10
|
+
"isEmpty", "size", "length",
|
|
11
|
+
"stream", "parallelStream", "forEach", "map", "filter", "collect",
|
|
12
|
+
"orElse", "orElseGet", "orElseThrow", "of", "ofNullable",
|
|
13
|
+
"add", "append", "remove", "contains", "put", "putAll",
|
|
14
|
+
"addAll", "removeAll", "containsAll", "containsKey", "containsValue",
|
|
15
|
+
"entrySet", "keySet", "values", "iterator", "hasNext", "next",
|
|
16
|
+
# Logging
|
|
17
|
+
"log", "debug", "info", "warn", "error", "trace",
|
|
18
|
+
# Common short helpers that create false-positive edges
|
|
19
|
+
"get", "set", "apply", "accept", "test",
|
|
20
|
+
"run", "call", "execute", "invoke",
|
|
21
|
+
"build", "create", "from", "parse", "format",
|
|
22
|
+
"close", "open", "init", "start", "stop", "reset",
|
|
23
|
+
"read", "write", "flush", "clear",
|
|
24
|
+
"supply", "compose", "andThen",
|
|
25
|
+
# Builder / accessor patterns
|
|
26
|
+
"builder", "toBuilder", "newBuilder",
|
|
27
|
+
"getName", "setName", "getValue", "setValue",
|
|
28
|
+
"getId", "setId", "getType", "setType",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Minimum method name length for fuzzy (global name+arity) fallback.
|
|
32
|
+
# Shorter names are too ambiguous for unresolved-receiver resolution.
|
|
33
|
+
MIN_FUZZY_NAME_LEN = 4
|
|
@@ -74,9 +74,12 @@ def clear_overlay(store, project: str | None = None) -> list[str]:
|
|
|
74
74
|
cleared: list[str] = []
|
|
75
75
|
for project_id in targets:
|
|
76
76
|
overlay_store.clear_project(project_id)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
try:
|
|
78
|
+
meta = store.get_project_metadata(project_id)
|
|
79
|
+
if meta:
|
|
80
|
+
store.set_project_overlay_dirty(project_id, False)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
LOGGER.warning("clear_overlay: could not update DB dirty flag for %s (%s); overlay files cleared", project_id, exc)
|
|
80
83
|
cleared.append(project_id)
|
|
81
84
|
return cleared
|
|
82
85
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codespine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Local Java code intelligence indexer backed by a graph database
|
|
5
5
|
Author: CodeSpine contributors
|
|
6
6
|
License: MIT License
|
|
@@ -267,7 +267,7 @@ codespine guide --json # structured JSON for tooling
|
|
|
267
267
|
| `detect_dead_code(limit, project, strict)` | Methods with no callers (Java-aware exemptions). |
|
|
268
268
|
| `trace_execution_flows(entry_symbol, max_depth, project)` | Execution paths from entry points. |
|
|
269
269
|
| `get_symbol_community(symbol)` | Architectural community cluster for a symbol. |
|
|
270
|
-
| `get_change_coupling(
|
|
270
|
+
| `get_change_coupling(days, min_strength, min_cochanges)` | Files that changed together in the last N days (default 5). |
|
|
271
271
|
|
|
272
272
|
**Git**
|
|
273
273
|
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""Noise filters for call graph generation."""
|
|
2
|
-
|
|
3
|
-
NOISE_METHOD_NAMES = {
|
|
4
|
-
"print",
|
|
5
|
-
"println",
|
|
6
|
-
"printf",
|
|
7
|
-
"hashCode",
|
|
8
|
-
"equals",
|
|
9
|
-
"toString",
|
|
10
|
-
"getClass",
|
|
11
|
-
"notify",
|
|
12
|
-
"notifyAll",
|
|
13
|
-
"wait",
|
|
14
|
-
"clone",
|
|
15
|
-
"finalize",
|
|
16
|
-
"compareTo",
|
|
17
|
-
"isEmpty",
|
|
18
|
-
"size",
|
|
19
|
-
"length",
|
|
20
|
-
"stream",
|
|
21
|
-
"parallelStream",
|
|
22
|
-
"forEach",
|
|
23
|
-
"map",
|
|
24
|
-
"filter",
|
|
25
|
-
"collect",
|
|
26
|
-
"orElse",
|
|
27
|
-
"orElseGet",
|
|
28
|
-
"add",
|
|
29
|
-
"append",
|
|
30
|
-
"remove",
|
|
31
|
-
"contains",
|
|
32
|
-
"log",
|
|
33
|
-
"debug",
|
|
34
|
-
"info",
|
|
35
|
-
"warn",
|
|
36
|
-
"error",
|
|
37
|
-
}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|