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.
Files changed (61) hide show
  1. {codespine-0.7.2 → codespine-0.8.0}/PKG-INFO +2 -2
  2. {codespine-0.7.2 → codespine-0.8.0}/README.md +1 -1
  3. {codespine-0.7.2 → codespine-0.8.0}/codespine/__init__.py +1 -1
  4. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/crossmodule.py +26 -28
  5. {codespine-0.7.2 → codespine-0.8.0}/codespine/db/schema.py +6 -1
  6. {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/call_resolver.py +7 -3
  7. {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/engine.py +29 -1
  8. {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/java_parser.py +23 -4
  9. {codespine-0.7.2 → codespine-0.8.0}/codespine/mcp/server.py +88 -11
  10. codespine-0.8.0/codespine/noise/blocklist.py +33 -0
  11. {codespine-0.7.2 → codespine-0.8.0}/codespine/watch/watcher.py +6 -3
  12. {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/PKG-INFO +2 -2
  13. {codespine-0.7.2 → codespine-0.8.0}/pyproject.toml +1 -1
  14. codespine-0.7.2/codespine/noise/blocklist.py +0 -37
  15. {codespine-0.7.2 → codespine-0.8.0}/LICENSE +0 -0
  16. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/__init__.py +0 -0
  17. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/community.py +0 -0
  18. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/context.py +0 -0
  19. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/coupling.py +0 -0
  20. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/deadcode.py +0 -0
  21. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/flow.py +0 -0
  22. {codespine-0.7.2 → codespine-0.8.0}/codespine/analysis/impact.py +0 -0
  23. {codespine-0.7.2 → codespine-0.8.0}/codespine/cli.py +0 -0
  24. {codespine-0.7.2 → codespine-0.8.0}/codespine/config.py +0 -0
  25. {codespine-0.7.2 → codespine-0.8.0}/codespine/db/__init__.py +0 -0
  26. {codespine-0.7.2 → codespine-0.8.0}/codespine/db/store.py +0 -0
  27. {codespine-0.7.2 → codespine-0.8.0}/codespine/diff/__init__.py +0 -0
  28. {codespine-0.7.2 → codespine-0.8.0}/codespine/diff/branch_diff.py +0 -0
  29. {codespine-0.7.2 → codespine-0.8.0}/codespine/guide.py +0 -0
  30. {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/__init__.py +0 -0
  31. {codespine-0.7.2 → codespine-0.8.0}/codespine/indexer/symbol_builder.py +0 -0
  32. {codespine-0.7.2 → codespine-0.8.0}/codespine/mcp/__init__.py +0 -0
  33. {codespine-0.7.2 → codespine-0.8.0}/codespine/noise/__init__.py +0 -0
  34. {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/__init__.py +0 -0
  35. {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/git_state.py +0 -0
  36. {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/merge.py +0 -0
  37. {codespine-0.7.2 → codespine-0.8.0}/codespine/overlay/store.py +0 -0
  38. {codespine-0.7.2 → codespine-0.8.0}/codespine/search/__init__.py +0 -0
  39. {codespine-0.7.2 → codespine-0.8.0}/codespine/search/bm25.py +0 -0
  40. {codespine-0.7.2 → codespine-0.8.0}/codespine/search/fuzzy.py +0 -0
  41. {codespine-0.7.2 → codespine-0.8.0}/codespine/search/hybrid.py +0 -0
  42. {codespine-0.7.2 → codespine-0.8.0}/codespine/search/rrf.py +0 -0
  43. {codespine-0.7.2 → codespine-0.8.0}/codespine/search/vector.py +0 -0
  44. {codespine-0.7.2 → codespine-0.8.0}/codespine/watch/__init__.py +0 -0
  45. {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/SOURCES.txt +0 -0
  46. {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/dependency_links.txt +0 -0
  47. {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/entry_points.txt +0 -0
  48. {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/requires.txt +0 -0
  49. {codespine-0.7.2 → codespine-0.8.0}/codespine.egg-info/top_level.txt +0 -0
  50. {codespine-0.7.2 → codespine-0.8.0}/gindex.py +0 -0
  51. {codespine-0.7.2 → codespine-0.8.0}/setup.cfg +0 -0
  52. {codespine-0.7.2 → codespine-0.8.0}/tests/test_branch_diff_normalize.py +0 -0
  53. {codespine-0.7.2 → codespine-0.8.0}/tests/test_call_resolver.py +0 -0
  54. {codespine-0.7.2 → codespine-0.8.0}/tests/test_community_detection.py +0 -0
  55. {codespine-0.7.2 → codespine-0.8.0}/tests/test_deadcode.py +0 -0
  56. {codespine-0.7.2 → codespine-0.8.0}/tests/test_index_and_hybrid.py +0 -0
  57. {codespine-0.7.2 → codespine-0.8.0}/tests/test_java_parser.py +0 -0
  58. {codespine-0.7.2 → codespine-0.8.0}/tests/test_multimodule_index.py +0 -0
  59. {codespine-0.7.2 → codespine-0.8.0}/tests/test_overlay.py +0 -0
  60. {codespine-0.7.2 → codespine-0.8.0}/tests/test_search_ranking.py +0 -0
  61. {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.7.2
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(months, min_strength, min_cochanges)` | Files that historically change together. |
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(months, min_strength, min_cochanges)` | Files that historically change together. |
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
 
@@ -1,4 +1,4 @@
1
1
  """CodeSpine package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.7.2"
4
+ __version__ = "0.8.0"
@@ -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 — Type-reference fallback (confidence 0.4)
21
- For every *public, non-constructor* method in the referenced class that
22
- received NO name-match edge, create ONE low-confidence edge from the
23
- referencing method. This prevents methods that are genuinely used
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: fallback for unmatched public dst methods
169
- for dm in dst_methods:
170
- if dm["mid"] in matched_dst_mids:
171
- continue
172
- if dm.get("is_ctor"):
173
- continue
174
- mods = dm.get("modifiers") or []
175
- mod_strs = {str(m).strip() for m in mods} if mods else set()
176
- if "private" in mod_strs:
177
- continue
178
-
179
- pair = (sm["mid"], dm["mid"])
180
- if pair in seen:
181
- continue
182
- seen.add(pair)
183
- try:
184
- store.add_call(
185
- sm["mid"], dm["mid"],
186
- 0.4, "cross_module_type_ref",
187
- )
188
- new_edges += 1
189
- except Exception as exc:
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, months 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
- project_id = os.path.basename(root_path)
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
- field_map[_text(node)] = current_type
213
- return field_map
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=_extract_field_types(node),
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
- result = analyze_impact(store, symbol, max_depth=max_depth, project=project)
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
- result = symbol_community(store, symbol)
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 "method".
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
- total = len(classes) + len(methods)
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 lower(m.name) = lower($q)
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
- meta = store.get_project_metadata(project_id)
78
- if meta:
79
- store.set_project_overlay_dirty(project_id, False)
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.7.2
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(months, min_strength, min_cochanges)` | Files that historically change together. |
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codespine"
7
- version = "0.7.2"
7
+ version = "0.8.0"
8
8
  description = "Local Java code intelligence indexer backed by a graph database"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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