codespine 1.0.12__tar.gz → 1.0.13__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 (80) hide show
  1. {codespine-1.0.12 → codespine-1.0.13}/PKG-INFO +2 -2
  2. {codespine-1.0.12 → codespine-1.0.13}/README.md +1 -1
  3. {codespine-1.0.12 → codespine-1.0.13}/codespine/__init__.py +1 -1
  4. {codespine-1.0.12 → codespine-1.0.13}/codespine/search/hybrid.py +52 -12
  5. {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/PKG-INFO +2 -2
  6. {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/SOURCES.txt +1 -0
  7. {codespine-1.0.12 → codespine-1.0.13}/pyproject.toml +1 -1
  8. codespine-1.0.13/tests/test_hybrid_search.py +89 -0
  9. {codespine-1.0.12 → codespine-1.0.13}/LICENSE +0 -0
  10. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/__init__.py +0 -0
  11. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/community.py +0 -0
  12. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/context.py +0 -0
  13. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/coupling.py +0 -0
  14. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/crossmodule.py +0 -0
  15. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/deadcode.py +0 -0
  16. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/flow.py +0 -0
  17. {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/impact.py +0 -0
  18. {codespine-1.0.12 → codespine-1.0.13}/codespine/cache/__init__.py +0 -0
  19. {codespine-1.0.12 → codespine-1.0.13}/codespine/cache/result_cache.py +0 -0
  20. {codespine-1.0.12 → codespine-1.0.13}/codespine/cli.py +0 -0
  21. {codespine-1.0.12 → codespine-1.0.13}/codespine/config.py +0 -0
  22. {codespine-1.0.12 → codespine-1.0.13}/codespine/db/__init__.py +0 -0
  23. {codespine-1.0.12 → codespine-1.0.13}/codespine/db/_cypher_compat.py +0 -0
  24. {codespine-1.0.12 → codespine-1.0.13}/codespine/db/duckdb_store.py +0 -0
  25. {codespine-1.0.12 → codespine-1.0.13}/codespine/db/schema.py +0 -0
  26. {codespine-1.0.12 → codespine-1.0.13}/codespine/db/store.py +0 -0
  27. {codespine-1.0.12 → codespine-1.0.13}/codespine/diff/__init__.py +0 -0
  28. {codespine-1.0.12 → codespine-1.0.13}/codespine/diff/branch_diff.py +0 -0
  29. {codespine-1.0.12 → codespine-1.0.13}/codespine/guide.py +0 -0
  30. {codespine-1.0.12 → codespine-1.0.13}/codespine/health.py +0 -0
  31. {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/__init__.py +0 -0
  32. {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/call_resolver.py +0 -0
  33. {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/di_resolver.py +0 -0
  34. {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/engine.py +0 -0
  35. {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/java_parser.py +0 -0
  36. {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/symbol_builder.py +0 -0
  37. {codespine-1.0.12 → codespine-1.0.13}/codespine/mcp/__init__.py +0 -0
  38. {codespine-1.0.12 → codespine-1.0.13}/codespine/mcp/server.py +0 -0
  39. {codespine-1.0.12 → codespine-1.0.13}/codespine/noise/__init__.py +0 -0
  40. {codespine-1.0.12 → codespine-1.0.13}/codespine/noise/blocklist.py +0 -0
  41. {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/__init__.py +0 -0
  42. {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/git_state.py +0 -0
  43. {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/merge.py +0 -0
  44. {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/store.py +0 -0
  45. {codespine-1.0.12 → codespine-1.0.13}/codespine/project_state.py +0 -0
  46. {codespine-1.0.12 → codespine-1.0.13}/codespine/search/__init__.py +0 -0
  47. {codespine-1.0.12 → codespine-1.0.13}/codespine/search/bm25.py +0 -0
  48. {codespine-1.0.12 → codespine-1.0.13}/codespine/search/fuzzy.py +0 -0
  49. {codespine-1.0.12 → codespine-1.0.13}/codespine/search/rrf.py +0 -0
  50. {codespine-1.0.12 → codespine-1.0.13}/codespine/search/vector.py +0 -0
  51. {codespine-1.0.12 → codespine-1.0.13}/codespine/sharding/__init__.py +0 -0
  52. {codespine-1.0.12 → codespine-1.0.13}/codespine/sharding/router.py +0 -0
  53. {codespine-1.0.12 → codespine-1.0.13}/codespine/sharding/store.py +0 -0
  54. {codespine-1.0.12 → codespine-1.0.13}/codespine/tasks.py +0 -0
  55. {codespine-1.0.12 → codespine-1.0.13}/codespine/watch/__init__.py +0 -0
  56. {codespine-1.0.12 → codespine-1.0.13}/codespine/watch/git_hook.py +0 -0
  57. {codespine-1.0.12 → codespine-1.0.13}/codespine/watch/watcher.py +0 -0
  58. {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/dependency_links.txt +0 -0
  59. {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/entry_points.txt +0 -0
  60. {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/requires.txt +0 -0
  61. {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/top_level.txt +0 -0
  62. {codespine-1.0.12 → codespine-1.0.13}/gindex.py +0 -0
  63. {codespine-1.0.12 → codespine-1.0.13}/setup.cfg +0 -0
  64. {codespine-1.0.12 → codespine-1.0.13}/tests/test_branch_diff_normalize.py +0 -0
  65. {codespine-1.0.12 → codespine-1.0.13}/tests/test_call_resolver.py +0 -0
  66. {codespine-1.0.12 → codespine-1.0.13}/tests/test_community_detection.py +0 -0
  67. {codespine-1.0.12 → codespine-1.0.13}/tests/test_cypher_compat.py +0 -0
  68. {codespine-1.0.12 → codespine-1.0.13}/tests/test_deadcode.py +0 -0
  69. {codespine-1.0.12 → codespine-1.0.13}/tests/test_duckdb_store.py +0 -0
  70. {codespine-1.0.12 → codespine-1.0.13}/tests/test_health.py +0 -0
  71. {codespine-1.0.12 → codespine-1.0.13}/tests/test_index_and_hybrid.py +0 -0
  72. {codespine-1.0.12 → codespine-1.0.13}/tests/test_java_parser.py +0 -0
  73. {codespine-1.0.12 → codespine-1.0.13}/tests/test_multimodule_index.py +0 -0
  74. {codespine-1.0.12 → codespine-1.0.13}/tests/test_overlay.py +0 -0
  75. {codespine-1.0.12 → codespine-1.0.13}/tests/test_parse_resilience.py +0 -0
  76. {codespine-1.0.12 → codespine-1.0.13}/tests/test_result_cache.py +0 -0
  77. {codespine-1.0.12 → codespine-1.0.13}/tests/test_search_ranking.py +0 -0
  78. {codespine-1.0.12 → codespine-1.0.13}/tests/test_sharding.py +0 -0
  79. {codespine-1.0.12 → codespine-1.0.13}/tests/test_store_recovery.py +0 -0
  80. {codespine-1.0.12 → codespine-1.0.13}/tests/test_tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 1.0.12
3
+ Version: 1.0.13
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -66,7 +66,7 @@ Dynamic: license-file
66
66
 
67
67
  # CodeSpine
68
68
 
69
- **v1.0.12** — Local Java code intelligence for coding agents, backed by a graph database.
69
+ **v1.0.13** — Local Java code intelligence for coding agents, backed by a graph database.
70
70
 
71
71
  CodeSpine cuts token burn for coding agents working on Java codebases.
72
72
 
@@ -1,6 +1,6 @@
1
1
  # CodeSpine
2
2
 
3
- **v1.0.12** — Local Java code intelligence for coding agents, backed by a graph database.
3
+ **v1.0.13** — Local Java code intelligence for coding agents, backed by a graph database.
4
4
 
5
5
  CodeSpine cuts token burn for coding agents working on Java codebases.
6
6
 
@@ -1,4 +1,4 @@
1
1
  """CodeSpine package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "1.0.12"
4
+ __version__ = "1.0.13"
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import os
5
+ from itertools import product
4
6
 
5
7
  from codespine.overlay.merge import merged_symbol_records
6
8
  from codespine.search.bm25 import rank_bm25
@@ -8,6 +10,8 @@ from codespine.search.fuzzy import rank_fuzzy
8
10
  from codespine.search.rrf import reciprocal_rank_fusion
9
11
  from codespine.search.vector import _load_model, rank_semantic
10
12
 
13
+ LOGGER = logging.getLogger(__name__)
14
+
11
15
  _LOW_CONFIDENCE_THRESHOLD = 0.05
12
16
  _SNIPPET_CONTEXT_LINES = 2 # lines above and below the symbol declaration
13
17
 
@@ -29,6 +33,46 @@ def _read_snippet(file_path: str, line: int, context: int = _SNIPPET_CONTEXT_LIN
29
33
  return None
30
34
 
31
35
 
36
+ def _context_entry(
37
+ community: dict | None = None,
38
+ flow: dict | None = None,
39
+ ) -> dict[str, object]:
40
+ return {
41
+ "community_id": community.get("community_id") if community else None,
42
+ "community_label": community.get("community_label") if community else None,
43
+ "flow_id": flow.get("flow_id") if flow else None,
44
+ "flow_kind": flow.get("flow_kind") if flow else None,
45
+ "flow_depth": flow.get("flow_depth") if flow else None,
46
+ }
47
+
48
+
49
+ def _load_symbol_context(store, symbol_id: str) -> list[dict[str, object]]:
50
+ community_rows = store.query_records(
51
+ """
52
+ MATCH (s:Symbol {id: $sid})-[:IN_COMMUNITY]->(c:Community)
53
+ RETURN c.id as community_id, c.label as community_label
54
+ LIMIT 3
55
+ """,
56
+ {"sid": symbol_id},
57
+ )
58
+ flow_rows = store.query_records(
59
+ """
60
+ MATCH (s:Symbol {id: $sid})-[f:IN_FLOW]->(fl:Flow)
61
+ RETURN fl.id as flow_id, fl.kind as flow_kind, f.depth as flow_depth
62
+ LIMIT 3
63
+ """,
64
+ {"sid": symbol_id},
65
+ )
66
+
67
+ if community_rows and flow_rows:
68
+ return [_context_entry(community, flow) for community, flow in product(community_rows, flow_rows)][:3]
69
+ if community_rows:
70
+ return [_context_entry(community=community) for community in community_rows][:3]
71
+ if flow_rows:
72
+ return [_context_entry(flow=flow) for flow in flow_rows][:3]
73
+ return []
74
+
75
+
32
76
  def hybrid_search(store, query: str, k: int = 20, project: str | None = None) -> list[dict]:
33
77
  overlay_store = getattr(store, "overlay_store", None)
34
78
  if overlay_store is not None:
@@ -105,19 +149,15 @@ def hybrid_search(store, query: str, k: int = 20, project: str | None = None) ->
105
149
  results.sort(key=lambda x: x["score"], reverse=True)
106
150
  top_k = results[:k]
107
151
 
108
- # Attach architectural context in same response.
152
+ # Attach architectural context in the same response. This is best-effort:
153
+ # a context query failure must not hide ranked symbol results.
109
154
  for item in top_k:
110
- ctx = store.query_records(
111
- """
112
- MATCH (s:Symbol {id: $sid})-[:IN_COMMUNITY]->(c:Community)
113
- OPTIONAL MATCH (s)-[f:IN_FLOW]->(fl:Flow)
114
- RETURN c.id as community_id, c.label as community_label,
115
- fl.id as flow_id, fl.kind as flow_kind, f.depth as flow_depth
116
- LIMIT 3
117
- """,
118
- {"sid": item["id"]},
119
- )
120
- item["context"] = ctx
155
+ try:
156
+ item["context"] = _load_symbol_context(store, item["id"])
157
+ except Exception as exc: # noqa: BLE001
158
+ LOGGER.warning("Unable to load architectural context for %s: %s", item.get("id"), exc)
159
+ item["context"] = []
160
+ item["context_warning"] = "Architectural context unavailable for this result."
121
161
 
122
162
  # Attach source code snippets (3–5 lines around the declaration) to the
123
163
  # top results so agents have immediate context without reading the file.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 1.0.12
3
+ Version: 1.0.13
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -66,7 +66,7 @@ Dynamic: license-file
66
66
 
67
67
  # CodeSpine
68
68
 
69
- **v1.0.12** — Local Java code intelligence for coding agents, backed by a graph database.
69
+ **v1.0.13** — Local Java code intelligence for coding agents, backed by a graph database.
70
70
 
71
71
  CodeSpine cuts token burn for coding agents working on Java codebases.
72
72
 
@@ -65,6 +65,7 @@ tests/test_cypher_compat.py
65
65
  tests/test_deadcode.py
66
66
  tests/test_duckdb_store.py
67
67
  tests/test_health.py
68
+ tests/test_hybrid_search.py
68
69
  tests/test_index_and_hybrid.py
69
70
  tests/test_java_parser.py
70
71
  tests/test_multimodule_index.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codespine"
7
- version = "1.0.12"
7
+ version = "1.0.13"
8
8
  description = "Local Java code intelligence indexer backed by a graph database"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from codespine.analysis.context import build_symbol_context
8
+ from codespine.search.hybrid import hybrid_search
9
+
10
+
11
+ class _FailingContextStore:
12
+ def query_records(self, query: str, params: dict | None = None) -> list[dict]:
13
+ if "RETURN s.id as id" in query:
14
+ return [
15
+ {
16
+ "id": "s1",
17
+ "kind": "class",
18
+ "name": "Foo",
19
+ "fqname": "com.example.Foo",
20
+ "embedding": None,
21
+ "line": 1,
22
+ "file_id": "f1",
23
+ "file_path": "/tmp/Foo.java",
24
+ "project_id": "app",
25
+ "is_test": False,
26
+ }
27
+ ]
28
+ if "IN_COMMUNITY" in query or "IN_FLOW" in query:
29
+ raise RuntimeError("context exploded")
30
+ return []
31
+
32
+
33
+ def test_hybrid_search_degrades_gracefully_when_context_lookup_fails():
34
+ results = hybrid_search(_FailingContextStore(), "Foo", k=1)
35
+
36
+ assert len(results) == 1
37
+ assert results[0]["name"] == "Foo"
38
+ assert results[0]["context"] == []
39
+ assert results[0]["context_warning"] == "Architectural context unavailable for this result."
40
+
41
+
42
+ def test_build_symbol_context_keeps_search_candidates_when_context_lookup_fails(monkeypatch):
43
+ monkeypatch.setattr("codespine.analysis.context.analyze_impact", lambda *args, **kwargs: {"resolved_to": []})
44
+ monkeypatch.setattr("codespine.analysis.context.symbol_community", lambda *args, **kwargs: {"matches": []})
45
+ monkeypatch.setattr("codespine.analysis.context.trace_execution_flows", lambda *args, **kwargs: [])
46
+
47
+ result = build_symbol_context(_FailingContextStore(), "Foo")
48
+
49
+ assert result["focus"]["name"] == "Foo"
50
+ assert len(result["search_candidates"]) == 1
51
+ assert result["search_candidates"][0]["context"] == []
52
+ assert "context_warning" in result["search_candidates"][0]
53
+
54
+
55
+ def test_hybrid_search_returns_flow_depth_from_duckdb_context(tmp_path: Path):
56
+ pytest.importorskip("duckdb")
57
+
58
+ from codespine.db.duckdb_store import DuckDBStore
59
+
60
+ store = DuckDBStore(
61
+ db_path_override=str(tmp_path / "db"),
62
+ snapshot_path_override=str(tmp_path / "db_read"),
63
+ )
64
+ store.upsert_project("app", "/app")
65
+ store.upsert_file("f1", "/app/src/main/java/com/example/OrderService.java", "app", False, "abc")
66
+ store.upsert_symbols_batch(
67
+ [
68
+ {
69
+ "id": "s1",
70
+ "kind": "class",
71
+ "name": "OrderService",
72
+ "fqname": "com.example.OrderService",
73
+ "file_id": "f1",
74
+ "line": 1,
75
+ "col": 1,
76
+ "embedding": None,
77
+ }
78
+ ]
79
+ )
80
+ store.set_community("comm1", "Ordering", 0.9, ["s1"])
81
+ store.set_flow("flow1", "s1", "entry", [("s1", 0)])
82
+
83
+ results = hybrid_search(store, "OrderService", k=1, project="app")
84
+
85
+ assert len(results) == 1
86
+ assert results[0]["name"] == "OrderService"
87
+ assert results[0]["context"]
88
+ assert any(item.get("community_label") == "Ordering" for item in results[0]["context"])
89
+ assert any(item.get("flow_depth") == 0 for item in results[0]["context"])
File without changes
File without changes
File without changes
File without changes