codespine 0.5.4__tar.gz → 0.5.5__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.5.4 → codespine-0.5.5}/PKG-INFO +1 -1
- {codespine-0.5.4 → codespine-0.5.5}/codespine/__init__.py +1 -1
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/impact.py +83 -41
- {codespine-0.5.4 → codespine-0.5.5}/codespine/cli.py +52 -4
- {codespine-0.5.4 → codespine-0.5.5}/codespine/config.py +4 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/db/schema.py +5 -2
- {codespine-0.5.4 → codespine-0.5.5}/codespine/db/store.py +137 -1
- {codespine-0.5.4 → codespine-0.5.5}/codespine/indexer/engine.py +160 -68
- {codespine-0.5.4 → codespine-0.5.5}/codespine/mcp/server.py +154 -63
- codespine-0.5.5/codespine/overlay/__init__.py +23 -0
- codespine-0.5.5/codespine/overlay/git_state.py +35 -0
- codespine-0.5.5/codespine/overlay/merge.py +189 -0
- codespine-0.5.5/codespine/overlay/store.py +492 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/search/hybrid.py +26 -23
- codespine-0.5.5/codespine/watch/watcher.py +261 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine.egg-info/PKG-INFO +1 -1
- {codespine-0.5.4 → codespine-0.5.5}/codespine.egg-info/SOURCES.txt +5 -0
- {codespine-0.5.4 → codespine-0.5.5}/pyproject.toml +1 -1
- codespine-0.5.5/tests/test_overlay.py +231 -0
- codespine-0.5.4/codespine/watch/watcher.py +0 -75
- {codespine-0.5.4 → codespine-0.5.5}/LICENSE +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/README.md +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/community.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/context.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/coupling.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/crossmodule.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/deadcode.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/analysis/flow.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/db/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/diff/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/diff/branch_diff.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/indexer/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/indexer/call_resolver.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/indexer/java_parser.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/indexer/symbol_builder.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/mcp/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/noise/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/noise/blocklist.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/search/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/search/bm25.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/search/fuzzy.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/search/rrf.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/search/vector.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine/watch/__init__.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine.egg-info/dependency_links.txt +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine.egg-info/entry_points.txt +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine.egg-info/requires.txt +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/codespine.egg-info/top_level.txt +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/gindex.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/setup.cfg +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_branch_diff_normalize.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_call_resolver.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_community_detection.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_deadcode.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_index_and_hybrid.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_java_parser.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_multimodule_index.py +0 -0
- {codespine-0.5.4 → codespine-0.5.5}/tests/test_search_ranking.py +0 -0
|
@@ -2,22 +2,36 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict, deque
|
|
4
4
|
|
|
5
|
+
from codespine.overlay.merge import merged_call_edges, merged_method_records, merged_symbol_records
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def _resolve_symbol_ids(store, symbol_query: str, project: str | None = None) -> list[str]:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
overlay_store = getattr(store, "overlay_store", None)
|
|
10
|
+
if overlay_store is not None:
|
|
11
|
+
recs = []
|
|
12
|
+
needle = symbol_query.lower()
|
|
13
|
+
for rec in merged_symbol_records(store, overlay_store, project=project):
|
|
14
|
+
name = str(rec.get("name") or "").lower()
|
|
15
|
+
fqname = str(rec.get("fqname") or "").lower()
|
|
16
|
+
if rec.get("id") == symbol_query or name == needle or fqname == needle or needle in fqname:
|
|
17
|
+
recs.append({"id": rec["id"]})
|
|
18
|
+
if len(recs) >= 50:
|
|
19
|
+
break
|
|
20
|
+
else:
|
|
21
|
+
project_clause = "AND f.project_id = $proj" if project else ""
|
|
22
|
+
params: dict = {"q": symbol_query}
|
|
23
|
+
if project:
|
|
24
|
+
params["proj"] = project
|
|
25
|
+
recs = store.query_records(
|
|
26
|
+
f"""
|
|
27
|
+
MATCH (s:Symbol), (f:File)
|
|
28
|
+
WHERE s.file_id = f.id {project_clause}
|
|
29
|
+
AND (s.id = $q OR lower(s.name) = lower($q) OR lower(s.fqname) = lower($q) OR lower(s.fqname) CONTAINS lower($q))
|
|
30
|
+
RETURN s.id as id
|
|
31
|
+
LIMIT 50
|
|
32
|
+
""",
|
|
33
|
+
params,
|
|
34
|
+
)
|
|
21
35
|
return [r["id"] for r in recs]
|
|
22
36
|
|
|
23
37
|
|
|
@@ -30,15 +44,21 @@ def _resolve_method_metadata(store, method_ids: list[str]) -> dict[str, dict]:
|
|
|
30
44
|
"""
|
|
31
45
|
if not method_ids:
|
|
32
46
|
return {}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
overlay_store = getattr(store, "overlay_store", None)
|
|
48
|
+
if overlay_store is not None:
|
|
49
|
+
recs = [r for r in merged_method_records(store, overlay_store) if r.get("id") in set(method_ids)]
|
|
50
|
+
for rec in recs:
|
|
51
|
+
rec["fqname"] = rec.get("signature")
|
|
52
|
+
else:
|
|
53
|
+
recs = store.query_records(
|
|
54
|
+
"""
|
|
55
|
+
MATCH (m:Method), (c:Class), (f:File)
|
|
56
|
+
WHERE m.id IN $ids AND m.class_id = c.id AND c.file_id = f.id
|
|
57
|
+
RETURN m.id as id, m.name as name, m.signature as fqname,
|
|
58
|
+
c.fqcn as class_fqcn, f.path as file_path, f.project_id as project_id
|
|
59
|
+
""",
|
|
60
|
+
{"ids": method_ids},
|
|
61
|
+
)
|
|
42
62
|
return {r["id"]: r for r in recs}
|
|
43
63
|
|
|
44
64
|
|
|
@@ -47,16 +67,33 @@ def analyze_impact(store, symbol_query: str, max_depth: int = 4, project: str |
|
|
|
47
67
|
if not target_symbol_ids:
|
|
48
68
|
return {"target": symbol_query, "depth_groups": {"1": [], "2": [], "3+": []}}
|
|
49
69
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
overlay_store = getattr(store, "overlay_store", None)
|
|
71
|
+
if overlay_store is not None:
|
|
72
|
+
methods = merged_method_records(store, overlay_store, project=project)
|
|
73
|
+
symbols = merged_symbol_records(store, overlay_store, project=project)
|
|
74
|
+
fqname_and_file_to_method = {
|
|
75
|
+
(f"{rec.get('class_fqcn')}#{rec.get('signature')}", rec.get("file_id")): rec["id"]
|
|
76
|
+
for rec in methods
|
|
77
|
+
}
|
|
78
|
+
symbol_to_method = {}
|
|
79
|
+
for rec in symbols:
|
|
80
|
+
if rec.get("kind") != "method":
|
|
81
|
+
continue
|
|
82
|
+
method_key = (rec.get("fqname"), rec.get("file_id"))
|
|
83
|
+
method_id = fqname_and_file_to_method.get(method_key)
|
|
84
|
+
if method_id:
|
|
85
|
+
symbol_to_method[rec["id"]] = method_id
|
|
86
|
+
else:
|
|
87
|
+
symbol_to_method = {
|
|
88
|
+
r["sid"]: r["mid"]
|
|
89
|
+
for r in store.query_records(
|
|
90
|
+
"""
|
|
91
|
+
MATCH (s:Symbol),(m:Method)
|
|
92
|
+
WHERE s.kind = 'method' AND s.fqname CONTAINS m.signature
|
|
93
|
+
RETURN s.id as sid, m.id as mid
|
|
94
|
+
"""
|
|
95
|
+
)
|
|
96
|
+
}
|
|
60
97
|
|
|
61
98
|
target_method_ids = [symbol_to_method[sid] for sid in target_symbol_ids if sid in symbol_to_method]
|
|
62
99
|
if not target_method_ids:
|
|
@@ -64,14 +101,19 @@ def analyze_impact(store, symbol_query: str, max_depth: int = 4, project: str |
|
|
|
64
101
|
|
|
65
102
|
# Load all call edges – cross-project callers are included intentionally so
|
|
66
103
|
# impact analysis surfaces inter-module dependencies.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
if overlay_store is not None:
|
|
105
|
+
edges = merged_call_edges(store, overlay_store, project=project)
|
|
106
|
+
for edge in edges:
|
|
107
|
+
edge["edge_type"] = "CALLS"
|
|
108
|
+
else:
|
|
109
|
+
edges = store.query_records(
|
|
110
|
+
"""
|
|
111
|
+
MATCH (a:Method)-[r:CALLS]->(b:Method)
|
|
112
|
+
RETURN a.id as src, b.id as dst, 'CALLS' as edge_type,
|
|
113
|
+
coalesce(r.confidence, 0.5) as confidence,
|
|
114
|
+
coalesce(r.reason, 'unknown') as reason
|
|
115
|
+
"""
|
|
116
|
+
)
|
|
75
117
|
|
|
76
118
|
reverse_adj: dict[str, list[dict]] = defaultdict(list)
|
|
77
119
|
for edge in edges:
|
|
@@ -24,7 +24,7 @@ from codespine.diff.branch_diff import compare_branches
|
|
|
24
24
|
from codespine.indexer.engine import JavaIndexer
|
|
25
25
|
from codespine.mcp.server import build_mcp_server
|
|
26
26
|
from codespine.search.hybrid import hybrid_search
|
|
27
|
-
from codespine.watch.watcher import run_watch_mode
|
|
27
|
+
from codespine.watch.watcher import clear_overlay, get_overlay_status, promote_overlay, run_watch_mode
|
|
28
28
|
|
|
29
29
|
logging.basicConfig(filename=SETTINGS.log_file, level=logging.INFO)
|
|
30
30
|
LOGGER = logging.getLogger(__name__)
|
|
@@ -414,10 +414,23 @@ def coupling(months: int, min_strength: float, min_cochanges: int, as_json: bool
|
|
|
414
414
|
@main.command()
|
|
415
415
|
@click.option("--path", default=".", show_default=True, type=click.Path(exists=True))
|
|
416
416
|
@click.option("--global-interval", default=30, show_default=True, type=int)
|
|
417
|
-
|
|
417
|
+
@click.option(
|
|
418
|
+
"--overlay-debounce-ms",
|
|
419
|
+
default=SETTINGS.default_overlay_debounce_ms,
|
|
420
|
+
show_default=True,
|
|
421
|
+
type=int,
|
|
422
|
+
)
|
|
423
|
+
@click.option("--promote-on-commit/--no-promote-on-commit", default=True, show_default=True)
|
|
424
|
+
def watch(path: str, global_interval: int, overlay_debounce_ms: int, promote_on_commit: bool) -> None:
|
|
418
425
|
"""Live re-indexing and periodic global analysis refresh."""
|
|
419
426
|
store = GraphStore(read_only=False)
|
|
420
|
-
run_watch_mode(
|
|
427
|
+
run_watch_mode(
|
|
428
|
+
store,
|
|
429
|
+
os.path.abspath(path),
|
|
430
|
+
global_interval=global_interval,
|
|
431
|
+
overlay_debounce_ms=overlay_debounce_ms,
|
|
432
|
+
promote_on_commit=promote_on_commit,
|
|
433
|
+
)
|
|
421
434
|
|
|
422
435
|
|
|
423
436
|
@main.command()
|
|
@@ -521,6 +534,8 @@ def status(as_json: bool) -> None:
|
|
|
521
534
|
pid = int(f.read().strip())
|
|
522
535
|
except Exception:
|
|
523
536
|
pid = None
|
|
537
|
+
store = GraphStore(read_only=True)
|
|
538
|
+
overlay = get_overlay_status(store)
|
|
524
539
|
payload = {
|
|
525
540
|
"running": running,
|
|
526
541
|
"pid": pid,
|
|
@@ -528,10 +543,41 @@ def status(as_json: bool) -> None:
|
|
|
528
543
|
"db_path": SETTINGS.db_path,
|
|
529
544
|
"db_size_bytes": _db_size_bytes(SETTINGS.db_path),
|
|
530
545
|
"log_file": SETTINGS.log_file,
|
|
546
|
+
"overlay_dir": SETTINGS.overlay_dir,
|
|
547
|
+
"overlay_projects": overlay,
|
|
531
548
|
}
|
|
532
549
|
_echo_json(payload, as_json)
|
|
533
550
|
|
|
534
551
|
|
|
552
|
+
@main.command("overlay-status")
|
|
553
|
+
@click.option("--project", default=None)
|
|
554
|
+
@click.option("--json", "as_json", is_flag=True)
|
|
555
|
+
def overlay_status_cmd(project: str | None, as_json: bool) -> None:
|
|
556
|
+
"""Show dirty overlay status by project/module."""
|
|
557
|
+
store = GraphStore(read_only=True)
|
|
558
|
+
_echo_json(get_overlay_status(store, project=project), as_json)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@main.command("overlay-clear")
|
|
562
|
+
@click.option("--project", default=None)
|
|
563
|
+
@click.option("--json", "as_json", is_flag=True)
|
|
564
|
+
def overlay_clear_cmd(project: str | None, as_json: bool) -> None:
|
|
565
|
+
"""Clear dirty overlay data without touching the committed base index."""
|
|
566
|
+
store = GraphStore(read_only=False)
|
|
567
|
+
result = {"cleared": clear_overlay(store, project=project)}
|
|
568
|
+
_echo_json(result, as_json)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@main.command("overlay-promote")
|
|
572
|
+
@click.option("--project", default=None)
|
|
573
|
+
@click.option("--json", "as_json", is_flag=True)
|
|
574
|
+
def overlay_promote_cmd(project: str | None, as_json: bool) -> None:
|
|
575
|
+
"""Promote dirty overlay changes into the committed base index now."""
|
|
576
|
+
store = GraphStore(read_only=False)
|
|
577
|
+
result = {"promoted": promote_overlay(store, project=project, require_head_change=False)}
|
|
578
|
+
_echo_json(result, as_json)
|
|
579
|
+
|
|
580
|
+
|
|
535
581
|
@main.command()
|
|
536
582
|
@click.argument("query")
|
|
537
583
|
@click.option("--json", "as_json", is_flag=True)
|
|
@@ -552,7 +598,7 @@ def clean(force: bool) -> None:
|
|
|
552
598
|
if not force and not click.confirm("Remove local CodeSpine DB, PID, and logs?"):
|
|
553
599
|
click.echo("Aborted.")
|
|
554
600
|
return
|
|
555
|
-
for path in [SETTINGS.pid_file, SETTINGS.log_file, SETTINGS.db_path]:
|
|
601
|
+
for path in [SETTINGS.pid_file, SETTINGS.log_file, SETTINGS.db_path, SETTINGS.overlay_dir]:
|
|
556
602
|
if not os.path.exists(path):
|
|
557
603
|
continue
|
|
558
604
|
if os.path.isdir(path):
|
|
@@ -591,6 +637,7 @@ def clear_project_cmd(project_id: str, allow_running: bool) -> None:
|
|
|
591
637
|
project_path = recs[0].get("path", "")
|
|
592
638
|
store.clear_analysis_artifacts()
|
|
593
639
|
store.clear_project(project_id)
|
|
640
|
+
store.overlay_store.clear_project(project_id)
|
|
594
641
|
meta_path = JavaIndexer._meta_cache_path(project_id)
|
|
595
642
|
if os.path.exists(meta_path):
|
|
596
643
|
try:
|
|
@@ -615,6 +662,7 @@ def clear_index_cmd(allow_running: bool) -> None:
|
|
|
615
662
|
store = GraphStore(read_only=False)
|
|
616
663
|
projects = store.query_records("MATCH (p:Project) RETURN p.id as id")
|
|
617
664
|
store.rebuild_empty_db()
|
|
665
|
+
store.overlay_store.clear_all()
|
|
618
666
|
for p in projects:
|
|
619
667
|
meta_path = JavaIndexer._meta_cache_path(p["id"])
|
|
620
668
|
if os.path.exists(meta_path):
|
|
@@ -9,15 +9,19 @@ class Settings:
|
|
|
9
9
|
log_file: str = os.path.expanduser("~/.codespine.log")
|
|
10
10
|
embedding_cache_path: str = os.path.expanduser("~/.codespine_embedding_cache.json")
|
|
11
11
|
index_meta_dir: str = os.path.expanduser("~/.codespine_index_meta")
|
|
12
|
+
overlay_dir: str = os.path.expanduser("~/.codespine_overlay")
|
|
12
13
|
embedding_model: str = "BAAI/bge-small-en-v1.5"
|
|
13
14
|
vector_dim: int = 384
|
|
14
15
|
rrf_k: int = 60
|
|
15
16
|
semantic_candidate_pool: int = 2000
|
|
16
17
|
write_batch_size: int = 500
|
|
18
|
+
index_file_batch_size: int = 64
|
|
19
|
+
edge_write_batch_size: int = 2000
|
|
17
20
|
default_coupling_months: int = 6
|
|
18
21
|
default_min_coupling_strength: float = 0.3
|
|
19
22
|
default_min_cochanges: int = 3
|
|
20
23
|
default_global_interval_s: int = 30
|
|
24
|
+
default_overlay_debounce_ms: int = 1500
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
SETTINGS = Settings()
|
|
@@ -10,7 +10,7 @@ NODE_TABLES: list[tuple[str, str]] = [
|
|
|
10
10
|
("SchemaMeta", "CREATE NODE TABLE SchemaMeta(key STRING, value STRING, PRIMARY KEY (key))"),
|
|
11
11
|
(
|
|
12
12
|
"Project",
|
|
13
|
-
"CREATE NODE TABLE Project(id STRING, path STRING, language STRING, indexed_at STRING, PRIMARY KEY (id))",
|
|
13
|
+
"CREATE NODE TABLE Project(id STRING, path STRING, language STRING, indexed_at STRING, indexed_commit STRING, overlay_dirty BOOL, PRIMARY KEY (id))",
|
|
14
14
|
),
|
|
15
15
|
(
|
|
16
16
|
"File",
|
|
@@ -81,5 +81,8 @@ def ensure_schema(conn) -> None:
|
|
|
81
81
|
|
|
82
82
|
_safe_execute(
|
|
83
83
|
conn,
|
|
84
|
-
"MERGE (s:SchemaMeta {key: 'schema_version'}) SET s.value = '
|
|
84
|
+
"MERGE (s:SchemaMeta {key: 'schema_version'}) SET s.value = '4'",
|
|
85
85
|
)
|
|
86
|
+
|
|
87
|
+
_safe_execute(conn, "ALTER TABLE Project ADD indexed_commit STRING DEFAULT ''")
|
|
88
|
+
_safe_execute(conn, "ALTER TABLE Project ADD overlay_dirty BOOL DEFAULT false")
|
|
@@ -28,6 +28,9 @@ class GraphStore:
|
|
|
28
28
|
def __post_init__(self) -> None:
|
|
29
29
|
db_path = SETTINGS.db_path
|
|
30
30
|
self._tls: threading.local = threading.local()
|
|
31
|
+
from codespine.overlay.store import OverlayStore
|
|
32
|
+
|
|
33
|
+
self.overlay_store = OverlayStore()
|
|
31
34
|
try:
|
|
32
35
|
self.db = self._open_db(db_path)
|
|
33
36
|
except Exception as exc:
|
|
@@ -94,10 +97,77 @@ class GraphStore:
|
|
|
94
97
|
|
|
95
98
|
def upsert_project(self, project_id: str, path: str) -> None:
|
|
96
99
|
self.execute(
|
|
97
|
-
"
|
|
100
|
+
"""
|
|
101
|
+
MERGE (p:Project {id: $id})
|
|
102
|
+
SET p.path = $path,
|
|
103
|
+
p.language = 'java',
|
|
104
|
+
p.indexed_at = $ts,
|
|
105
|
+
p.indexed_commit = coalesce(p.indexed_commit, ''),
|
|
106
|
+
p.overlay_dirty = coalesce(p.overlay_dirty, false)
|
|
107
|
+
""",
|
|
98
108
|
{"id": project_id, "path": path, "ts": str(int(time.time()))},
|
|
99
109
|
)
|
|
100
110
|
|
|
111
|
+
def set_project_overlay_dirty(self, project_id: str, dirty: bool) -> None:
|
|
112
|
+
self.execute(
|
|
113
|
+
"MATCH (p:Project {id: $id}) SET p.overlay_dirty = $dirty",
|
|
114
|
+
{"id": project_id, "dirty": bool(dirty)},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def set_project_indexed_commit(self, project_id: str, commit: str) -> None:
|
|
118
|
+
self.execute(
|
|
119
|
+
"""
|
|
120
|
+
MATCH (p:Project {id: $id})
|
|
121
|
+
SET p.indexed_commit = $commit,
|
|
122
|
+
p.indexed_at = $ts
|
|
123
|
+
""",
|
|
124
|
+
{"id": project_id, "commit": commit, "ts": str(int(time.time()))},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def get_project_metadata(self, project_id: str) -> dict[str, Any] | None:
|
|
128
|
+
recs = self.query_records(
|
|
129
|
+
"""
|
|
130
|
+
MATCH (p:Project)
|
|
131
|
+
WHERE p.id = $pid
|
|
132
|
+
RETURN p.id as id,
|
|
133
|
+
p.path as path,
|
|
134
|
+
p.language as language,
|
|
135
|
+
p.indexed_at as indexed_at,
|
|
136
|
+
p.indexed_commit as indexed_commit,
|
|
137
|
+
p.overlay_dirty as overlay_dirty
|
|
138
|
+
LIMIT 1
|
|
139
|
+
""",
|
|
140
|
+
{"pid": project_id},
|
|
141
|
+
)
|
|
142
|
+
return recs[0] if recs else None
|
|
143
|
+
|
|
144
|
+
def list_project_metadata(self) -> list[dict[str, Any]]:
|
|
145
|
+
return self.query_records(
|
|
146
|
+
"""
|
|
147
|
+
MATCH (p:Project)
|
|
148
|
+
RETURN p.id as id,
|
|
149
|
+
p.path as path,
|
|
150
|
+
p.language as language,
|
|
151
|
+
p.indexed_at as indexed_at,
|
|
152
|
+
p.indexed_commit as indexed_commit,
|
|
153
|
+
p.overlay_dirty as overlay_dirty
|
|
154
|
+
ORDER BY p.id
|
|
155
|
+
"""
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def project_has_embeddings(self, project_id: str) -> bool:
|
|
159
|
+
recs = self.query_records(
|
|
160
|
+
"""
|
|
161
|
+
MATCH (s:Symbol), (f:File)
|
|
162
|
+
WHERE s.file_id = f.id
|
|
163
|
+
AND f.project_id = $pid
|
|
164
|
+
AND s.embedding IS NOT NULL
|
|
165
|
+
RETURN count(s) as count
|
|
166
|
+
""",
|
|
167
|
+
{"pid": project_id},
|
|
168
|
+
)
|
|
169
|
+
return bool(recs and int(recs[0].get("count") or 0) > 0)
|
|
170
|
+
|
|
101
171
|
def project_file_hashes(self, project_id: str) -> dict[str, dict[str, str]]:
|
|
102
172
|
recs = self.query_records(
|
|
103
173
|
"""
|
|
@@ -164,6 +234,16 @@ class GraphStore:
|
|
|
164
234
|
},
|
|
165
235
|
)
|
|
166
236
|
|
|
237
|
+
def upsert_files_batch(self, records: list[dict[str, Any]]) -> None:
|
|
238
|
+
for record in records:
|
|
239
|
+
self.upsert_file(
|
|
240
|
+
file_id=record["id"],
|
|
241
|
+
path=record["path"],
|
|
242
|
+
project_id=record["project_id"],
|
|
243
|
+
is_test=bool(record["is_test"]),
|
|
244
|
+
digest=record["hash"],
|
|
245
|
+
)
|
|
246
|
+
|
|
167
247
|
def upsert_class(self, class_id: str, fqcn: str, name: str, package: str, file_id: str) -> None:
|
|
168
248
|
self.execute(
|
|
169
249
|
"""
|
|
@@ -179,6 +259,16 @@ class GraphStore:
|
|
|
179
259
|
},
|
|
180
260
|
)
|
|
181
261
|
|
|
262
|
+
def upsert_classes_batch(self, records: list[dict[str, Any]]) -> None:
|
|
263
|
+
for record in records:
|
|
264
|
+
self.upsert_class(
|
|
265
|
+
class_id=record["id"],
|
|
266
|
+
fqcn=record["fqcn"],
|
|
267
|
+
name=record["name"],
|
|
268
|
+
package=record["package"],
|
|
269
|
+
file_id=record["file_id"],
|
|
270
|
+
)
|
|
271
|
+
|
|
182
272
|
def upsert_method(
|
|
183
273
|
self,
|
|
184
274
|
method_id: str,
|
|
@@ -217,6 +307,19 @@ class GraphStore:
|
|
|
217
307
|
{"cid": class_id, "mid": method_id},
|
|
218
308
|
)
|
|
219
309
|
|
|
310
|
+
def upsert_methods_batch(self, records: list[dict[str, Any]]) -> None:
|
|
311
|
+
for record in records:
|
|
312
|
+
self.upsert_method(
|
|
313
|
+
method_id=record["id"],
|
|
314
|
+
class_id=record["class_id"],
|
|
315
|
+
name=record["name"],
|
|
316
|
+
signature=record["signature"],
|
|
317
|
+
return_type=record["return_type"],
|
|
318
|
+
modifiers=record["modifiers"],
|
|
319
|
+
is_constructor=bool(record["is_constructor"]),
|
|
320
|
+
is_test=bool(record["is_test"]),
|
|
321
|
+
)
|
|
322
|
+
|
|
220
323
|
def upsert_symbol(
|
|
221
324
|
self,
|
|
222
325
|
symbol_id: str,
|
|
@@ -255,6 +358,19 @@ class GraphStore:
|
|
|
255
358
|
{"fid": file_id, "sid": symbol_id},
|
|
256
359
|
)
|
|
257
360
|
|
|
361
|
+
def upsert_symbols_batch(self, records: list[dict[str, Any]]) -> None:
|
|
362
|
+
for record in records:
|
|
363
|
+
self.upsert_symbol(
|
|
364
|
+
symbol_id=record["id"],
|
|
365
|
+
kind=record["kind"],
|
|
366
|
+
name=record["name"],
|
|
367
|
+
fqname=record["fqname"],
|
|
368
|
+
file_id=record["file_id"],
|
|
369
|
+
line=int(record["line"]),
|
|
370
|
+
col=int(record["col"]),
|
|
371
|
+
embedding=record.get("embedding"),
|
|
372
|
+
)
|
|
373
|
+
|
|
258
374
|
def add_call(self, source_id: str, target_id: str, confidence: float, reason: str) -> None:
|
|
259
375
|
self.execute(
|
|
260
376
|
"""
|
|
@@ -269,6 +385,15 @@ class GraphStore:
|
|
|
269
385
|
},
|
|
270
386
|
)
|
|
271
387
|
|
|
388
|
+
def add_calls_batch(self, records: list[dict[str, Any]]) -> None:
|
|
389
|
+
for record in records:
|
|
390
|
+
self.add_call(
|
|
391
|
+
source_id=record["source_id"],
|
|
392
|
+
target_id=record["target_id"],
|
|
393
|
+
confidence=float(record["confidence"]),
|
|
394
|
+
reason=record["reason"],
|
|
395
|
+
)
|
|
396
|
+
|
|
272
397
|
def add_reference(self, rel: str, src_label: str, src_id: str, dst_label: str, dst_id: str, confidence: float) -> None:
|
|
273
398
|
if rel not in {"REFERENCES_TYPE", "IMPLEMENTS", "OVERRIDES"}:
|
|
274
399
|
return
|
|
@@ -278,6 +403,17 @@ class GraphStore:
|
|
|
278
403
|
)
|
|
279
404
|
self.execute(query, {"src_id": src_id, "dst_id": dst_id, "confidence": confidence})
|
|
280
405
|
|
|
406
|
+
def add_references_batch(self, records: list[dict[str, Any]]) -> None:
|
|
407
|
+
for record in records:
|
|
408
|
+
self.add_reference(
|
|
409
|
+
rel=record["rel"],
|
|
410
|
+
src_label=record["src_label"],
|
|
411
|
+
src_id=record["src_id"],
|
|
412
|
+
dst_label=record["dst_label"],
|
|
413
|
+
dst_id=record["dst_id"],
|
|
414
|
+
confidence=float(record["confidence"]),
|
|
415
|
+
)
|
|
416
|
+
|
|
281
417
|
def _recycle_conn(self) -> None:
|
|
282
418
|
"""Drop and recreate the per-thread connection to release buffer pages."""
|
|
283
419
|
try:
|