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.
- {codespine-1.0.12 → codespine-1.0.13}/PKG-INFO +2 -2
- {codespine-1.0.12 → codespine-1.0.13}/README.md +1 -1
- {codespine-1.0.12 → codespine-1.0.13}/codespine/__init__.py +1 -1
- {codespine-1.0.12 → codespine-1.0.13}/codespine/search/hybrid.py +52 -12
- {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/PKG-INFO +2 -2
- {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/SOURCES.txt +1 -0
- {codespine-1.0.12 → codespine-1.0.13}/pyproject.toml +1 -1
- codespine-1.0.13/tests/test_hybrid_search.py +89 -0
- {codespine-1.0.12 → codespine-1.0.13}/LICENSE +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/community.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/context.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/coupling.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/crossmodule.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/deadcode.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/flow.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/analysis/impact.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/cache/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/cache/result_cache.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/cli.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/config.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/db/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/db/_cypher_compat.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/db/duckdb_store.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/db/schema.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/db/store.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/diff/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/diff/branch_diff.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/guide.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/health.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/call_resolver.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/di_resolver.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/engine.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/java_parser.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/indexer/symbol_builder.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/mcp/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/mcp/server.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/noise/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/noise/blocklist.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/git_state.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/merge.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/overlay/store.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/project_state.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/search/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/search/bm25.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/search/fuzzy.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/search/rrf.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/search/vector.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/sharding/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/sharding/router.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/sharding/store.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/tasks.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/watch/__init__.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/watch/git_hook.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine/watch/watcher.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/dependency_links.txt +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/entry_points.txt +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/requires.txt +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/codespine.egg-info/top_level.txt +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/gindex.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/setup.cfg +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_branch_diff_normalize.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_call_resolver.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_community_detection.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_cypher_compat.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_deadcode.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_duckdb_store.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_health.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_index_and_hybrid.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_java_parser.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_multimodule_index.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_overlay.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_parse_resilience.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_result_cache.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_search_ranking.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_sharding.py +0 -0
- {codespine-1.0.12 → codespine-1.0.13}/tests/test_store_recovery.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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,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
|
-
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|