codecortex 0.8.2__tar.gz → 0.8.4__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.
Potentially problematic release.
This version of codecortex might be problematic. Click here for more details.
- {codecortex-0.8.2/src/codecortex.egg-info → codecortex-0.8.4}/PKG-INFO +1 -1
- {codecortex-0.8.2 → codecortex-0.8.4/src/codecortex.egg-info}/PKG-INFO +1 -1
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codecortex.egg-info/SOURCES.txt +2 -0
- codecortex-0.8.4/src/codeintel/__init__.py +1 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/__main__.py +27 -22
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/indexer.py +5 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/onboarding.py +1 -1
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/providers/semantic.py +16 -10
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/reindexer.py +3 -3
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/reset.py +37 -9
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/semantic_db.py +84 -14
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/server.py +53 -6
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_doctor.py +3 -3
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_e2e.py +1 -2
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_hardening.py +1 -1
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_integration.py +2 -2
- codecortex-0.8.4/tests/test_mcp_server.py +42 -0
- codecortex-0.8.4/tests/test_model_dimension.py +148 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_onboarding.py +1 -1
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_semantic_provider.py +4 -4
- codecortex-0.8.2/src/codeintel/__init__.py +0 -1
- {codecortex-0.8.2 → codecortex-0.8.4}/LICENSE +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/README.md +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/pyproject.toml +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/setup.cfg +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/auth.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/cache.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/config.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/doctor.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/gateway.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/http_server.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/injector.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/installer.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/mapper.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/metrics.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/policy.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/provider.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/searcher.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/src/codeintel/term.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_cache.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_chunking.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_config.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_enterprise.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_gateway.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_graph_provider.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_graph_real.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_http_auth.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_http_server.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_installer.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_lsp_real.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_mapper.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_never_raise.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_rbac.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_reindexer.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_rerank.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_reset.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_term.py +0 -0
- {codecortex-0.8.2 → codecortex-0.8.4}/tests/test_treesitter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.4
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.4
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.4"
|
|
@@ -110,28 +110,32 @@ def main() -> None:
|
|
|
110
110
|
from codeintel.semantic_db import SemanticDb, default_db_path
|
|
111
111
|
|
|
112
112
|
project_root = args.project_root or os.getcwd()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
db_path = default_db_path()
|
|
116
|
-
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
117
|
-
db = SemanticDb(db_path)
|
|
113
|
+
# Wrap the whole semantic pass so a setup failure (e.g. an unresolvable home dir →
|
|
114
|
+
# Path.home() raising) degrades with a message, like every other subcommand, not a traceback.
|
|
118
115
|
try:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
116
|
+
cfg = load_config(project_root)
|
|
117
|
+
db_path = default_db_path(str(cfg.get("model") or ""))
|
|
118
|
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
119
|
+
db = SemanticDb(db_path)
|
|
120
|
+
try:
|
|
121
|
+
db.init()
|
|
122
|
+
count = Indexer(
|
|
123
|
+
db,
|
|
124
|
+
model_name=str(cfg.get("model") or "BAAI/bge-small-en-v1.5"),
|
|
125
|
+
window=int(cfg.get("window", 20)),
|
|
126
|
+
stride=int(cfg.get("stride", 10)),
|
|
127
|
+
max_chunks=int(cfg.get("max_chunks", 500)),
|
|
128
|
+
max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
|
|
129
|
+
chunk_strategy=str(cfg.get("chunk_strategy", "syntax")),
|
|
130
|
+
).index(project_root)
|
|
131
|
+
if count > 0:
|
|
132
|
+
print(f"Indexed {count} chunks")
|
|
133
|
+
else:
|
|
134
|
+
print("Nothing new to index")
|
|
135
|
+
finally:
|
|
136
|
+
db.close()
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
print(f"index failed: {exc}")
|
|
135
139
|
|
|
136
140
|
# best-effort graph reindex
|
|
137
141
|
import shutil
|
|
@@ -242,8 +246,9 @@ def main() -> None:
|
|
|
242
246
|
state = "available" if available else "unavailable"
|
|
243
247
|
print(f" {engine:<10} {state}")
|
|
244
248
|
|
|
249
|
+
from codeintel.config import load_config
|
|
245
250
|
from codeintel.semantic_db import default_db_path
|
|
246
|
-
db_path = default_db_path()
|
|
251
|
+
db_path = default_db_path(str(load_config(project_root).get("model") or ""))
|
|
247
252
|
if os.path.exists(db_path):
|
|
248
253
|
import datetime
|
|
249
254
|
mtime = os.path.getmtime(db_path)
|
|
@@ -519,6 +519,11 @@ class Indexer:
|
|
|
519
519
|
break
|
|
520
520
|
try:
|
|
521
521
|
vec = embeddings[j]
|
|
522
|
+
# Create code_embeddings lazily, sized to this vector (self-dimensioning). A
|
|
523
|
+
# returned dim != len(vec) means the file already holds a different dimension
|
|
524
|
+
# (only reachable on a DEFAULT_MODEL size bump) — skip rather than corrupt/mix.
|
|
525
|
+
if self.db.ensure_embeddings_table(len(vec)) != len(vec):
|
|
526
|
+
continue
|
|
522
527
|
vec_bytes = struct.pack(f"{len(vec)}f", *vec)
|
|
523
528
|
# sqlite-vec's vec0 virtual table does NOT honor INSERT OR REPLACE — it raises
|
|
524
529
|
# UNIQUE on an existing chunk_id instead of replacing. So re-embedding a chunk
|
|
@@ -63,7 +63,7 @@ def _bounded_index(project_root: str, *, timeout_s: float, out) -> dict:
|
|
|
63
63
|
def _work() -> None:
|
|
64
64
|
try:
|
|
65
65
|
cfg = load_config(project_root)
|
|
66
|
-
db_path = default_db_path()
|
|
66
|
+
db_path = default_db_path(str(cfg.get("model") or ""))
|
|
67
67
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
68
68
|
db = SemanticDb(db_path)
|
|
69
69
|
try:
|
|
@@ -12,8 +12,6 @@ try:
|
|
|
12
12
|
except ImportError:
|
|
13
13
|
_DEPS_OK = False
|
|
14
14
|
|
|
15
|
-
_DB_PATH = pathlib.Path.home() / ".codeintel" / "semantic.db"
|
|
16
|
-
|
|
17
15
|
|
|
18
16
|
class SemanticProvider:
|
|
19
17
|
"""Real semantic search provider backed by SemanticDb and Searcher."""
|
|
@@ -23,9 +21,10 @@ class SemanticProvider:
|
|
|
23
21
|
return _DEPS_OK
|
|
24
22
|
|
|
25
23
|
def probe(self, project_root: str) -> dict:
|
|
26
|
-
"""Never-raise health check for the doctor. READ-ONLY
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
"""Never-raise health check for the doctor. READ-ONLY: it opens the db read-only and counts
|
|
25
|
+
this repo's chunks — it must NOT call SemanticDb.init() (a schema write) or LOAD fastembed.
|
|
26
|
+
It does resolve the project's ``model`` *name* (a cheap config read, no model load) to pick
|
|
27
|
+
the per-model cache file. ``repo_indexed`` is project-scoped (mirrors Searcher.has_index)."""
|
|
29
28
|
if not self.available:
|
|
30
29
|
return {
|
|
31
30
|
"installed": False, "runnable": False, "repo_indexed": False,
|
|
@@ -36,8 +35,10 @@ class SemanticProvider:
|
|
|
36
35
|
import sqlite3
|
|
37
36
|
|
|
38
37
|
try:
|
|
38
|
+
from codeintel.config import load_config
|
|
39
39
|
from codeintel.semantic_db import default_db_path
|
|
40
|
-
|
|
40
|
+
model = str(load_config(project_root).get("model") or "")
|
|
41
|
+
db_path = default_db_path(model)
|
|
41
42
|
except Exception:
|
|
42
43
|
db_path = ""
|
|
43
44
|
if not db_path or not os.path.exists(db_path):
|
|
@@ -59,8 +60,8 @@ class SemanticProvider:
|
|
|
59
60
|
except Exception as exc:
|
|
60
61
|
return {
|
|
61
62
|
"installed": True, "runnable": False, "repo_indexed": False,
|
|
62
|
-
"detail": f"semantic
|
|
63
|
-
"remediation": "
|
|
63
|
+
"detail": f"semantic cache present but unreadable ({type(exc).__name__})",
|
|
64
|
+
"remediation": f"codeintel reset {project_root} && codeintel index {project_root}",
|
|
64
65
|
}
|
|
65
66
|
if count > 0:
|
|
66
67
|
return {
|
|
@@ -95,11 +96,16 @@ class SemanticProvider:
|
|
|
95
96
|
from codeintel.indexer import Indexer
|
|
96
97
|
from codeintel.searcher import Searcher
|
|
97
98
|
|
|
99
|
+
from codeintel.semantic_db import default_db_path
|
|
100
|
+
|
|
98
101
|
cfg = load_config(project_root)
|
|
99
102
|
model = str(cfg.get("model") or "BAAI/bge-small-en-v1.5")
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
# Per-model cache file: index and search for this repo use the SAME model → same file,
|
|
105
|
+
# so a repo configured with a different model can never corrupt or wipe another's rows.
|
|
106
|
+
db_path = default_db_path(model)
|
|
107
|
+
pathlib.Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
db = SemanticDb(db_path)
|
|
103
109
|
db.init()
|
|
104
110
|
|
|
105
111
|
searcher = Searcher(db, model_name=model)
|
|
@@ -102,11 +102,11 @@ class Reindexer:
|
|
|
102
102
|
from codeintel.semantic_db import SemanticDb, default_db_path
|
|
103
103
|
from codeintel.indexer import Indexer
|
|
104
104
|
|
|
105
|
-
# Same per-
|
|
105
|
+
# Same per-model cache file the SemanticProvider reads — index and search must never diverge
|
|
106
106
|
# onto different files. Honor the project's config so the background pass indexes exactly
|
|
107
|
-
# like the inline and CLI paths (same model, window/stride,
|
|
107
|
+
# like the inline and CLI paths (same model → same file, plus window/stride, ceilings).
|
|
108
108
|
cfg = load_config(project_root)
|
|
109
|
-
db_path = default_db_path()
|
|
109
|
+
db_path = default_db_path(str(cfg.get("model") or ""))
|
|
110
110
|
pathlib.Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
111
111
|
db = SemanticDb(db_path)
|
|
112
112
|
try:
|
|
@@ -5,6 +5,7 @@ module is pure (dry-run by default via ``apply=False``).
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import glob
|
|
8
9
|
import os
|
|
9
10
|
import sqlite3
|
|
10
11
|
|
|
@@ -13,6 +14,16 @@ import sqlite_vec
|
|
|
13
14
|
from codeintel.semantic_db import default_db_path
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def _cache_files() -> list[str]:
|
|
18
|
+
"""Every per-model cache file (``semantic.db`` + ``semantic-<hash>.db``). Reset must sweep all
|
|
19
|
+
of them: a repo's rows can live in any model's file, and a model switch leaves orphans behind."""
|
|
20
|
+
base = os.path.dirname(default_db_path())
|
|
21
|
+
try:
|
|
22
|
+
return sorted(glob.glob(os.path.join(base, "semantic*.db")))
|
|
23
|
+
except Exception:
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
|
|
16
27
|
def _reset_scoped(project_root: str, path: str, apply: bool) -> dict:
|
|
17
28
|
real = os.path.realpath(str(project_root))
|
|
18
29
|
if not os.path.exists(path):
|
|
@@ -37,9 +48,12 @@ def _reset_scoped(project_root: str, path: str, apply: bool) -> dict:
|
|
|
37
48
|
count = int(row[0]) if row else 0
|
|
38
49
|
|
|
39
50
|
if apply:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
try:
|
|
52
|
+
conn.execute(
|
|
53
|
+
"DELETE FROM code_embeddings WHERE chunk_id IN "
|
|
54
|
+
"(SELECT chunk_id FROM chunk_hashes WHERE project_root=?)", (real,))
|
|
55
|
+
except Exception:
|
|
56
|
+
pass # code_embeddings is created lazily at first embed — may not exist yet
|
|
43
57
|
conn.execute("DELETE FROM chunk_hashes WHERE project_root=?", (real,))
|
|
44
58
|
conn.commit()
|
|
45
59
|
|
|
@@ -88,13 +102,27 @@ def run_reset(
|
|
|
88
102
|
apply: bool = False,
|
|
89
103
|
db_path: str | None = None,
|
|
90
104
|
) -> dict:
|
|
91
|
-
"""Drop indexed rows for ``project_root`` (or, with ``all_projects``, remove the whole
|
|
92
|
-
|
|
93
|
-
delete nothing. Never raises.
|
|
105
|
+
"""Drop indexed rows for ``project_root`` (or, with ``all_projects``, remove the whole cache —
|
|
106
|
+
every per-model db file plus their -wal/-shm siblings). ``apply=False`` is a dry-run: count
|
|
107
|
+
only, delete nothing. Never raises.
|
|
108
|
+
|
|
109
|
+
An explicit ``db_path`` operates on exactly that one file (the test seam / a targeted reset);
|
|
110
|
+
otherwise reset sweeps EVERY per-model cache file, so a repo's rows are cleared no matter which
|
|
111
|
+
model's file they landed in, and model-switch orphans are reclaimed."""
|
|
94
112
|
try:
|
|
95
|
-
|
|
113
|
+
if db_path is not None:
|
|
114
|
+
return _reset_all(db_path, apply) if all_projects else _reset_scoped(project_root, db_path, apply)
|
|
115
|
+
|
|
116
|
+
files = _cache_files() or [default_db_path()]
|
|
96
117
|
if all_projects:
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
count = sum(_reset_all(p, apply)["count"] for p in files)
|
|
119
|
+
verb = "removed" if apply else "would remove"
|
|
120
|
+
return {"ok": True, "mode": "all", "target": "ALL", "count": count,
|
|
121
|
+
"applied": bool(apply), "detail": f"{verb} {count} index file(s) across all models"}
|
|
122
|
+
real = os.path.realpath(str(project_root))
|
|
123
|
+
count = sum(_reset_scoped(project_root, p, apply)["count"] for p in files)
|
|
124
|
+
verb = "removed" if apply else "found"
|
|
125
|
+
return {"ok": True, "mode": "scoped", "target": real, "count": count, "applied": bool(apply),
|
|
126
|
+
"detail": f"{verb} {count} indexed chunk(s) for this project"}
|
|
99
127
|
except Exception as exc:
|
|
100
128
|
return {"ok": True, "applied": apply, "detail": f"reset-error: {type(exc).__name__}: {exc}"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import logging
|
|
4
5
|
import pathlib
|
|
6
|
+
import re
|
|
5
7
|
import sqlite3
|
|
6
8
|
|
|
7
9
|
import sqlite_vec
|
|
@@ -11,23 +13,45 @@ logger = logging.getLogger(__name__)
|
|
|
11
13
|
DEFAULT_MODEL = "BAAI/bge-small-en-v1.5"
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
def
|
|
15
|
-
"""The
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
def _base_dir() -> pathlib.Path:
|
|
17
|
+
"""The per-machine cache directory. A single seam so tests can redirect every model's db
|
|
18
|
+
file at once (patch this, not each computed path)."""
|
|
19
|
+
return pathlib.Path.home() / ".codeintel"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _model_slug(model: str) -> str:
|
|
23
|
+
# errors="replace" keeps this total (default_db_path promises it) even for a pathological
|
|
24
|
+
# model string with unpaired surrogates — unreachable via config, but the docstring says total.
|
|
25
|
+
return hashlib.sha256(model.strip().encode("utf-8", "replace")).hexdigest()[:12]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_db_path(model: str | None = None) -> str:
|
|
29
|
+
"""The per-machine semantic cache file for a given embedding ``model``. A sqlite-vec vec0 table
|
|
30
|
+
is single-dimension and different models' vectors are incompatible, so each model gets its OWN
|
|
31
|
+
file — different-model repos then coexist as separate files and can never corrupt or wipe each
|
|
32
|
+
other. The default model (and ``None``) map to the legacy ``semantic.db`` (zero migration); any
|
|
33
|
+
other model maps to ``semantic-<hash(model)>.db``.
|
|
34
|
+
|
|
35
|
+
Index and search for one repo MUST pass the same model → same file. Rows are still partitioned
|
|
36
|
+
by ``project_root`` WITHIN a shared-model file. Pure + total: any string yields a filename."""
|
|
37
|
+
base = _base_dir()
|
|
38
|
+
m = (model or "").strip()
|
|
39
|
+
if not m or m == DEFAULT_MODEL:
|
|
40
|
+
return str(base / "semantic.db")
|
|
41
|
+
return str(base / f"semantic-{_model_slug(m)}.db")
|
|
21
42
|
|
|
22
43
|
|
|
23
44
|
class SemanticDb:
|
|
24
45
|
"""DB layer: opens a SQLite connection, loads sqlite-vec, and owns schema creation."""
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
_DIM_RE = re.compile(r"float\s*\[\s*(\d+)\s*\]", re.IGNORECASE)
|
|
27
48
|
|
|
28
49
|
def __init__(self, db_path: str) -> None:
|
|
29
50
|
self.db_path = db_path
|
|
30
51
|
self._conn: sqlite3.Connection | None = None
|
|
52
|
+
# The vec0 embedding dimension, discovered lazily from the table / the first real vector
|
|
53
|
+
# (see ensure_embeddings_table) rather than hardcoded — so any model's size just works.
|
|
54
|
+
self.dimension: int | None = None
|
|
31
55
|
|
|
32
56
|
def conn(self) -> sqlite3.Connection:
|
|
33
57
|
if self._conn is None:
|
|
@@ -68,12 +92,10 @@ class SemanticDb:
|
|
|
68
92
|
except Exception:
|
|
69
93
|
pass
|
|
70
94
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
76
|
-
|
|
95
|
+
# chunk_hashes + indexes are created now; code_embeddings is created LAZILY at the first
|
|
96
|
+
# write, sized to the embedding model's real vector length (ensure_embeddings_table) — a
|
|
97
|
+
# vec0 table is single-dimension, so it can't be created before the model's size is known.
|
|
98
|
+
c.executescript("""
|
|
77
99
|
CREATE TABLE IF NOT EXISTS chunk_hashes (
|
|
78
100
|
chunk_id TEXT PRIMARY KEY,
|
|
79
101
|
project_root TEXT NOT NULL,
|
|
@@ -94,6 +116,54 @@ class SemanticDb:
|
|
|
94
116
|
""")
|
|
95
117
|
c.commit()
|
|
96
118
|
|
|
119
|
+
def _table_dim(self) -> int | None:
|
|
120
|
+
"""The existing code_embeddings vec0 dimension from the live schema (``FLOAT[N]``), or None
|
|
121
|
+
if the table is absent / unparseable."""
|
|
122
|
+
try:
|
|
123
|
+
row = self.conn().execute(
|
|
124
|
+
"SELECT sql FROM sqlite_master WHERE name = 'code_embeddings'"
|
|
125
|
+
).fetchone()
|
|
126
|
+
if not row or not row[0]:
|
|
127
|
+
return None
|
|
128
|
+
m = self._DIM_RE.search(str(row[0]))
|
|
129
|
+
return int(m.group(1)) if m else None
|
|
130
|
+
except Exception:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def ensure_embeddings_table(self, dim: int) -> int | None:
|
|
134
|
+
"""Ensure ``code_embeddings`` exists sized to ``dim`` (the embedding's true length). Returns
|
|
135
|
+
the table dimension (== dim) on success, or ``None`` when it already exists at a DIFFERENT
|
|
136
|
+
dimension — the caller then skips the write, never mixing dimensions and never wiping data.
|
|
137
|
+
The table self-dimensions from the real vector, so any model (incl. future/unknown ones)
|
|
138
|
+
just works. Never raises.
|
|
139
|
+
|
|
140
|
+
A dimension mismatch is only reachable on the default-model file when a release bumps
|
|
141
|
+
``DEFAULT_MODEL`` to a new-sized model (a non-default file is keyed by model, so its dim is
|
|
142
|
+
fixed); that release directs the user to ``codeintel reset`` once. Non-destructive here."""
|
|
143
|
+
try:
|
|
144
|
+
dim = int(dim)
|
|
145
|
+
if self.dimension is None:
|
|
146
|
+
self.dimension = self._table_dim()
|
|
147
|
+
if self.dimension == dim:
|
|
148
|
+
return dim
|
|
149
|
+
if self.dimension is not None:
|
|
150
|
+
logger.warning(
|
|
151
|
+
"embedding dimension %d != cache dimension %d — skipping write; run "
|
|
152
|
+
"`codeintel reset` to rebuild the semantic index for the new model",
|
|
153
|
+
dim, self.dimension,
|
|
154
|
+
)
|
|
155
|
+
return None
|
|
156
|
+
self.conn().execute(
|
|
157
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS code_embeddings USING vec0("
|
|
158
|
+
f"chunk_id TEXT PRIMARY KEY, embedding FLOAT[{dim}])"
|
|
159
|
+
)
|
|
160
|
+
self.conn().commit()
|
|
161
|
+
self.dimension = dim
|
|
162
|
+
return dim
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
logger.warning("ensure_embeddings_table failed: %s", exc)
|
|
165
|
+
return None
|
|
166
|
+
|
|
97
167
|
def delete_file_orphans(
|
|
98
168
|
self, project_root: str, file_path: str, keep_ids: set[str]
|
|
99
169
|
) -> int:
|
|
@@ -122,10 +122,18 @@ def code_status_handler(args: dict) -> dict:
|
|
|
122
122
|
if semantic_available:
|
|
123
123
|
model = DEFAULT_MODEL
|
|
124
124
|
if project_root:
|
|
125
|
+
try: # report THIS repo's configured model, not the machine default
|
|
126
|
+
from codeintel.config import load_config
|
|
127
|
+
model = str(load_config(project_root).get("model") or DEFAULT_MODEL)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
125
130
|
indexed = bool(SemanticProvider().probe(project_root).get("repo_indexed"))
|
|
126
131
|
else:
|
|
132
|
+
import glob
|
|
127
133
|
import os
|
|
128
|
-
|
|
134
|
+
# any per-model cache file (semantic.db / semantic-<hash>.db) counts as "indexed"
|
|
135
|
+
base = os.path.dirname(default_db_path())
|
|
136
|
+
indexed = bool(glob.glob(os.path.join(base, "semantic*.db")))
|
|
129
137
|
except Exception:
|
|
130
138
|
pass
|
|
131
139
|
|
|
@@ -210,10 +218,32 @@ def code_map_handler(args: dict) -> dict:
|
|
|
210
218
|
return {"ok": True, "path": None, "size_bytes": 0, "note": "map-error"}
|
|
211
219
|
|
|
212
220
|
|
|
221
|
+
# Surfaced by the MCP client to the model on connect (the standard `instructions` field): this is
|
|
222
|
+
# how codeintel becomes the *default* way an agent understands code after `codeintel install`,
|
|
223
|
+
# rather than just an available tool the agent may ignore in favor of grep/file-read.
|
|
224
|
+
_MCP_INSTRUCTIONS = (
|
|
225
|
+
"codeintel unifies three code-intelligence engines — graph (call/import structure), LSP "
|
|
226
|
+
"(precise symbols/references), and semantic (embedding search) — behind one never-raise "
|
|
227
|
+
"`code.query` tool.\n\n"
|
|
228
|
+
"Prefer `code.query` as your FIRST step for understanding an unfamiliar or large codebase, "
|
|
229
|
+
"instead of grepping or reading files one by one: use it for who-calls-X (callers), what-X-calls "
|
|
230
|
+
"(callees), impact of a change, where-a-symbol-is-defined, how a call chain flows, and "
|
|
231
|
+
"natural-language 'find the code that does Y' search. It is graph-augmented and ranked, so it "
|
|
232
|
+
"beats raw grep for locating and relating code.\n\n"
|
|
233
|
+
"Orient on a new repo with `code.map` (ranked architecture: top symbols, entry points, routes). "
|
|
234
|
+
"If results look empty, call `code.doctor` — it says exactly what to index or install. "
|
|
235
|
+
"`code.status` reports engine health.\n\n"
|
|
236
|
+
"Every result is a safe envelope: `ok` is always true; a null `result` with a `reason` means "
|
|
237
|
+
"'nothing found / not indexed yet', NOT an error — read the `reason`/`hint` and, if it says the "
|
|
238
|
+
"repo isn't indexed, that resolves on the first query or via `codeintel index`."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
213
242
|
def run() -> None:
|
|
243
|
+
from codeintel import __version__
|
|
214
244
|
from codeintel.logconfig import configure_logging
|
|
215
245
|
configure_logging() # logs to stderr; stdout is the MCP protocol channel
|
|
216
|
-
mcp = MCPServer(name="codeintel")
|
|
246
|
+
mcp = MCPServer(name="codeintel", version=__version__, instructions=_MCP_INSTRUCTIONS)
|
|
217
247
|
|
|
218
248
|
async def _code_query(
|
|
219
249
|
op: str = "",
|
|
@@ -235,9 +265,26 @@ def run() -> None:
|
|
|
235
265
|
async def _code_map(project_root: str = "", budget: int = 32768, inject: bool = False) -> dict:
|
|
236
266
|
return code_map_handler({"project_root": project_root, "budget": budget, "inject": inject})
|
|
237
267
|
|
|
238
|
-
mcp.add_tool(_code_query, name="code.query", description=
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
268
|
+
mcp.add_tool(_code_query, name="code.query", description=(
|
|
269
|
+
"Understand code across graph + LSP + semantic engines — prefer this over grep/file-read "
|
|
270
|
+
"for locating and relating code. ops: `search` (natural-language or symbol semantic search), "
|
|
271
|
+
"`symbol` (definition/signature), `callers`, `callees`, `impact`, `chain` (\"A->B\" call "
|
|
272
|
+
"path), `pattern` (graph-augmented grep), `overview` (architecture), `context` (fan-out). "
|
|
273
|
+
"`target` is the symbol/query; optional `engine` (auto|graph|lsp|semantic), `project_root`. "
|
|
274
|
+
"Never raises: `ok` is always true; a null `result` + `reason` means not-found/not-indexed."
|
|
275
|
+
))
|
|
276
|
+
mcp.add_tool(_code_status, name="code.status", description=(
|
|
277
|
+
"Which engines (graph/LSP/semantic) are available and whether this repo is indexed. Check "
|
|
278
|
+
"this first if code.query keeps returning nothing."
|
|
279
|
+
))
|
|
280
|
+
mcp.add_tool(_code_doctor, name="code.doctor", description=(
|
|
281
|
+
"Diagnose engine health + this repo's index status, with a concrete fix for each gap (what "
|
|
282
|
+
"to install or index). Run when code.query results look empty or an engine seems missing."
|
|
283
|
+
))
|
|
284
|
+
mcp.add_tool(_code_map, name="code.map", description=(
|
|
285
|
+
"Generate/refresh CODE_INTEL.md — a ranked architecture overview (node/edge counts, top "
|
|
286
|
+
"symbols by caller count, entry points, routes). A great first call to orient on an "
|
|
287
|
+
"unfamiliar repo."
|
|
288
|
+
))
|
|
242
289
|
|
|
243
290
|
anyio.run(mcp.run_stdio_async)
|
|
@@ -157,7 +157,7 @@ def test_semantic_probe_real_db_indexed(tmp_path, monkeypatch):
|
|
|
157
157
|
repo = tmp_path / "repo"
|
|
158
158
|
repo.mkdir()
|
|
159
159
|
_make_db(db_path, os.path.realpath(str(repo)))
|
|
160
|
-
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda: str(db_path))
|
|
160
|
+
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda *a, **k: str(db_path))
|
|
161
161
|
|
|
162
162
|
r = SemanticProvider().probe(str(repo))
|
|
163
163
|
assert r["installed"] is True and r["runnable"] is True and r["repo_indexed"] is True
|
|
@@ -172,7 +172,7 @@ def test_semantic_probe_real_db_indexed(tmp_path, monkeypatch):
|
|
|
172
172
|
|
|
173
173
|
def test_semantic_probe_no_db(tmp_path, monkeypatch):
|
|
174
174
|
monkeypatch.setattr("codeintel.providers.semantic._DEPS_OK", True)
|
|
175
|
-
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda: str(tmp_path / "missing.db"))
|
|
175
|
+
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda *a, **k: str(tmp_path / "missing.db"))
|
|
176
176
|
r = SemanticProvider().probe(str(tmp_path))
|
|
177
177
|
assert r["runnable"] is True and r["repo_indexed"] is False
|
|
178
178
|
assert "codeintel index" in r["remediation"]
|
|
@@ -182,7 +182,7 @@ def test_semantic_probe_corrupt_db(tmp_path, monkeypatch):
|
|
|
182
182
|
monkeypatch.setattr("codeintel.providers.semantic._DEPS_OK", True)
|
|
183
183
|
db_path = tmp_path / "semantic.db"
|
|
184
184
|
db_path.write_bytes(b"this is not a sqlite database")
|
|
185
|
-
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda: str(db_path))
|
|
185
|
+
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda *a, **k: str(db_path))
|
|
186
186
|
r = SemanticProvider().probe(str(tmp_path))
|
|
187
187
|
assert r["installed"] is True and r["runnable"] is False # unreadable → not runnable
|
|
188
188
|
assert r["remediation"]
|
|
@@ -58,8 +58,7 @@ def fixture_repo(tmp_path):
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def test_e2e_search_returns_ranked_result(fixture_repo, tmp_path, monkeypatch):
|
|
61
|
-
|
|
62
|
-
monkeypatch.setattr(_sem_mod, "_DB_PATH", db_path)
|
|
61
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
63
62
|
monkeypatch.setattr(_sem_mod, "_DEPS_OK", True)
|
|
64
63
|
|
|
65
64
|
with patch("fastembed.TextEmbedding", _FakeTextEmbedding):
|
|
@@ -24,7 +24,7 @@ def _patch_semantic(monkeypatch, tmp_path, has_index_value):
|
|
|
24
24
|
from codeintel.indexer import Indexer
|
|
25
25
|
from codeintel.searcher import Searcher
|
|
26
26
|
|
|
27
|
-
monkeypatch.setattr(
|
|
27
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
28
28
|
calls = {"index": 0}
|
|
29
29
|
|
|
30
30
|
def _fake_index(self, root):
|
|
@@ -134,7 +134,7 @@ def test_overview_auto_falls_back_to_lsp():
|
|
|
134
134
|
def test_empty_project_reports_no_index(tmp_path, monkeypatch):
|
|
135
135
|
import codeintel.providers.semantic as sem
|
|
136
136
|
empty = tmp_path / "empty"; empty.mkdir()
|
|
137
|
-
monkeypatch.setattr(
|
|
137
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
138
138
|
monkeypatch.setattr(sem, "_DEPS_OK", True)
|
|
139
139
|
with patch("fastembed.TextEmbedding", _FakeTextEmbedding):
|
|
140
140
|
r = SemanticProvider().build_result("search", "x", [], 0, str(empty))
|
|
@@ -150,7 +150,7 @@ def test_config_cosine_floor_reaches_searcher(tmp_path, monkeypatch):
|
|
|
150
150
|
'cosine_floor = 0.99\nrerank = "off"\nrerank_candidates = 7\n'
|
|
151
151
|
)
|
|
152
152
|
(tmp_path / "code.py").write_text("def f():\n return 1\n")
|
|
153
|
-
monkeypatch.setattr(
|
|
153
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
154
154
|
monkeypatch.setattr(sem, "_DEPS_OK", True)
|
|
155
155
|
|
|
156
156
|
captured = {}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""MCP server advertisement (0.8.4).
|
|
2
|
+
|
|
3
|
+
`codeintel install` only makes the tools *available*; for an agent to actually reach for them
|
|
4
|
+
instead of grep/file-read, the server must advertise itself — the standard MCP `instructions`
|
|
5
|
+
field plus tool descriptions that say when to use each. These assert that wiring.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import codeintel.server as srv
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_server_advertises_instructions_and_rich_tool_descriptions(monkeypatch):
|
|
13
|
+
captured: dict = {"kwargs": None, "tools": {}}
|
|
14
|
+
|
|
15
|
+
class _FakeMCP:
|
|
16
|
+
def __init__(self, **kwargs):
|
|
17
|
+
captured["kwargs"] = kwargs
|
|
18
|
+
|
|
19
|
+
def add_tool(self, fn, name=None, description=None):
|
|
20
|
+
captured["tools"][name] = description or ""
|
|
21
|
+
|
|
22
|
+
async def run_stdio_async(self): # pragma: no cover - never awaited (anyio.run stubbed)
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
monkeypatch.setattr(srv, "MCPServer", _FakeMCP)
|
|
26
|
+
monkeypatch.setattr(srv.anyio, "run", lambda *a, **k: None) # don't block on stdio
|
|
27
|
+
|
|
28
|
+
srv.run()
|
|
29
|
+
|
|
30
|
+
kw = captured["kwargs"]
|
|
31
|
+
assert kw["name"] == "codeintel"
|
|
32
|
+
assert kw.get("version") # server reports its version
|
|
33
|
+
instr = kw.get("instructions") or ""
|
|
34
|
+
assert "code.query" in instr
|
|
35
|
+
assert "grep" in instr.lower() # explicitly steers the agent away from grep/file-read
|
|
36
|
+
assert "reason" in instr # explains the never-raise safe-null envelope
|
|
37
|
+
|
|
38
|
+
tools = captured["tools"]
|
|
39
|
+
assert set(tools) == {"code.query", "code.status", "code.doctor", "code.map"}
|
|
40
|
+
# the primary tool sells when to use it, not the old throwaway one-liner
|
|
41
|
+
q = tools["code.query"]
|
|
42
|
+
assert "callers" in q and "grep" in q.lower() and len(q) > 100
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Embedding-model / vector-dimension handling (0.8.3).
|
|
2
|
+
|
|
3
|
+
Different embedding models produce incompatible vectors (often different lengths), and a
|
|
4
|
+
sqlite-vec vec0 table is single-dimension. The design isolates them PHYSICALLY: each model gets
|
|
5
|
+
its own cache file (``semantic-<hash>.db``), and the vec0 table self-dimensions from the first
|
|
6
|
+
real vector. Test #1 is the regression that caught the reverted global-wipe fix.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import glob
|
|
11
|
+
import os
|
|
12
|
+
from unittest.mock import patch
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from codeintel.indexer import Indexer
|
|
17
|
+
from codeintel.reset import run_reset
|
|
18
|
+
from codeintel.searcher import Searcher
|
|
19
|
+
from codeintel.semantic_db import DEFAULT_MODEL, SemanticDb, default_db_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fake(dim):
|
|
23
|
+
class _F:
|
|
24
|
+
def __init__(self, model_name=None):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def embed(self, texts):
|
|
28
|
+
return [np.full(dim, 0.1, dtype=np.float32) for _ in list(texts)]
|
|
29
|
+
return _F
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _seed(path, project_root_real, cid="c1"):
|
|
33
|
+
db = SemanticDb(path)
|
|
34
|
+
db.init()
|
|
35
|
+
db.conn().execute(
|
|
36
|
+
"INSERT INTO chunk_hashes(chunk_id, project_root, file_path, chunk_start, content_hash)"
|
|
37
|
+
" VALUES (?,?,?,?,?)", (cid, project_root_real, "f.py", 0, "h"))
|
|
38
|
+
db.conn().commit()
|
|
39
|
+
db.close()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --------------------------------------------------------------------------- #1 THE regression
|
|
43
|
+
|
|
44
|
+
def test_different_model_repos_do_not_wipe_each_other(tmp_path, monkeypatch):
|
|
45
|
+
# The reverted fix wiped the whole shared cache on a model mismatch; with two repos on
|
|
46
|
+
# different models that meant they perpetually destroyed each other. Per-model files make it
|
|
47
|
+
# physically impossible.
|
|
48
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
49
|
+
(tmp_path / "a").mkdir(); (tmp_path / "a" / "a.py").write_text("def a():\n return 1\n")
|
|
50
|
+
(tmp_path / "b").mkdir(); (tmp_path / "b" / "b.py").write_text("def b():\n return 2\n")
|
|
51
|
+
|
|
52
|
+
def index(repo, model, dim):
|
|
53
|
+
db = SemanticDb(default_db_path(model)); db.init()
|
|
54
|
+
with patch("fastembed.TextEmbedding", _fake(dim)):
|
|
55
|
+
Indexer(db, model_name=model).index(str(tmp_path / repo))
|
|
56
|
+
return db
|
|
57
|
+
|
|
58
|
+
# ping-pong: A(default,384) → B(other,768) → A → B, asserting each survives the other
|
|
59
|
+
for _ in range(2):
|
|
60
|
+
db_a = index("a", DEFAULT_MODEL, 384)
|
|
61
|
+
db_b = index("b", "other-model-768", 768)
|
|
62
|
+
assert Searcher(db_a, model_name=DEFAULT_MODEL).has_index(str(tmp_path / "a")), \
|
|
63
|
+
"repo A must survive repo B indexing under a different model"
|
|
64
|
+
assert Searcher(db_b, model_name="other-model-768").has_index(str(tmp_path / "b"))
|
|
65
|
+
|
|
66
|
+
# they live in DIFFERENT files
|
|
67
|
+
assert default_db_path(DEFAULT_MODEL) != default_db_path("other-model-768")
|
|
68
|
+
assert os.path.exists(tmp_path / "semantic.db") # A → default file
|
|
69
|
+
assert len(glob.glob(str(tmp_path / "semantic-*.db"))) == 1 # B → its own file
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# --------------------------------------------------------------------------- self-dimensioning
|
|
73
|
+
|
|
74
|
+
def test_table_self_dimensions_from_first_vector(tmp_path):
|
|
75
|
+
db = SemanticDb(str(tmp_path / "y.db")); db.init()
|
|
76
|
+
assert db._table_dim() is None # init does NOT create code_embeddings
|
|
77
|
+
assert db.ensure_embeddings_table(768) == 768
|
|
78
|
+
assert db._table_dim() == 768 # created at the real vector size, not 384
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_ensure_embeddings_table_skips_on_dimension_mismatch(tmp_path):
|
|
82
|
+
# the only in-file mismatch case (a DEFAULT_MODEL size bump): skip the write, never wipe
|
|
83
|
+
db = SemanticDb(str(tmp_path / "x.db")); db.init()
|
|
84
|
+
assert db.ensure_embeddings_table(384) == 384
|
|
85
|
+
assert db.ensure_embeddings_table(768) is None # refuses
|
|
86
|
+
assert db._table_dim() == 384 # table left intact, not recreated/wiped
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------------- path invariants
|
|
90
|
+
|
|
91
|
+
def test_default_db_path_invariants(tmp_path, monkeypatch):
|
|
92
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
93
|
+
d = default_db_path
|
|
94
|
+
legacy = str(tmp_path / "semantic.db")
|
|
95
|
+
assert d() == d(None) == d("") == d(" ") == d(DEFAULT_MODEL) == legacy # default → legacy file
|
|
96
|
+
assert d("other") != legacy and d("other").startswith(str(tmp_path / "semantic-"))
|
|
97
|
+
assert d("a") != d("b") and d("a") == d("a") # distinct models distinct, same stable
|
|
98
|
+
assert d("wéird/model:v2") .endswith(".db") # total: any string yields a filename
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --------------------------------------------------------------------------- reset across files
|
|
102
|
+
|
|
103
|
+
def test_scoped_reset_sweeps_all_model_files_but_spares_other_projects(tmp_path, monkeypatch):
|
|
104
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
105
|
+
real_a = os.path.realpath(str(tmp_path / "a"))
|
|
106
|
+
real_b = os.path.realpath(str(tmp_path / "b"))
|
|
107
|
+
# project A has rows in TWO model files; project B has rows in the default file
|
|
108
|
+
_seed(default_db_path(DEFAULT_MODEL), real_a, "a-default")
|
|
109
|
+
_seed(default_db_path("other"), real_a, "a-other")
|
|
110
|
+
db_b = SemanticDb(default_db_path(DEFAULT_MODEL)) # same default file as A's default rows
|
|
111
|
+
db_b.conn().execute(
|
|
112
|
+
"INSERT INTO chunk_hashes(chunk_id, project_root, file_path, chunk_start, content_hash)"
|
|
113
|
+
" VALUES (?,?,?,?,?)", ("b1", real_b, "g.py", 0, "h"))
|
|
114
|
+
db_b.conn().commit(); db_b.close()
|
|
115
|
+
|
|
116
|
+
res = run_reset(str(tmp_path / "a"), apply=True) # no db_path → sweep every model file
|
|
117
|
+
assert res["count"] == 2 # A's rows removed from BOTH files
|
|
118
|
+
|
|
119
|
+
surviving = SemanticDb(default_db_path(DEFAULT_MODEL)).conn().execute(
|
|
120
|
+
"SELECT COUNT(*) FROM chunk_hashes WHERE project_root=?", (real_b,)).fetchone()[0]
|
|
121
|
+
assert surviving == 1, "a scoped reset of A must leave project B untouched"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_probe_and_build_result_resolve_the_same_file(tmp_path, monkeypatch):
|
|
125
|
+
# regression for the divergence the CHANGELOG says it closed (build_result used a module
|
|
126
|
+
# _DB_PATH, probe used default_db_path()): with a NON-default model both must hit the same file,
|
|
127
|
+
# or probe reports "not indexed" for a repo build_result just indexed.
|
|
128
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
129
|
+
monkeypatch.setattr("codeintel.providers.semantic._DEPS_OK", True)
|
|
130
|
+
from codeintel.providers.semantic import SemanticProvider
|
|
131
|
+
repo = tmp_path / "r"; repo.mkdir()
|
|
132
|
+
(repo / ".codeintel.toml").write_text('model = "custom-384-model"\n')
|
|
133
|
+
(repo / "m.py").write_text("def f():\n return 1\n")
|
|
134
|
+
|
|
135
|
+
with patch("fastembed.TextEmbedding", _fake(384)):
|
|
136
|
+
SemanticProvider().build_result("search", "f", [], 0, str(repo)) # writes the model's file
|
|
137
|
+
assert os.path.exists(default_db_path("custom-384-model")) # not the default file
|
|
138
|
+
assert SemanticProvider().probe(str(repo))["repo_indexed"] is True # probe found the SAME file
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_reset_tolerates_absent_code_embeddings(tmp_path):
|
|
142
|
+
# code_embeddings is created lazily, so a seed-only db has none — reset must not choke on it
|
|
143
|
+
p = str(tmp_path / "x.db")
|
|
144
|
+
real = os.path.realpath(str(tmp_path))
|
|
145
|
+
_seed(p, real)
|
|
146
|
+
assert SemanticDb(p)._table_dim() is None # confirms no code_embeddings table
|
|
147
|
+
res = run_reset(str(tmp_path), apply=True, db_path=p)
|
|
148
|
+
assert res["count"] == 1 and "removed" in res["detail"]
|
|
@@ -59,7 +59,7 @@ def test_setup_index_real_db(tmp_path, monkeypatch):
|
|
|
59
59
|
(repo / "sample.py").write_text("def greet():\n return 'hello'\n")
|
|
60
60
|
|
|
61
61
|
db_path = tmp_path / "semantic.db"
|
|
62
|
-
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda: str(db_path))
|
|
62
|
+
monkeypatch.setattr("codeintel.semantic_db.default_db_path", lambda *a, **k: str(db_path))
|
|
63
63
|
|
|
64
64
|
report = onboarding.run_setup(str(repo), do_index=True, out=io.StringIO())
|
|
65
65
|
assert report["ok"] is True
|
|
@@ -97,7 +97,7 @@ def test_available_when_deps_present(monkeypatch):
|
|
|
97
97
|
def test_search_returns_matches(tmp_path, monkeypatch):
|
|
98
98
|
(tmp_path / "sample.py").write_text("def greet():\n return 'hello'\n")
|
|
99
99
|
db_path = tmp_path / "semantic.db"
|
|
100
|
-
monkeypatch.setattr("codeintel.
|
|
100
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
101
101
|
monkeypatch.setattr("codeintel.providers.semantic._DEPS_OK", True)
|
|
102
102
|
|
|
103
103
|
with patch("fastembed.TextEmbedding", _FakeTextEmbedding):
|
|
@@ -135,7 +135,7 @@ def test_empty_index_safe_null(tmp_path, monkeypatch):
|
|
|
135
135
|
empty_dir = tmp_path / "empty"
|
|
136
136
|
empty_dir.mkdir()
|
|
137
137
|
db_path = tmp_path / "semantic.db"
|
|
138
|
-
monkeypatch.setattr("codeintel.
|
|
138
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
139
139
|
monkeypatch.setattr("codeintel.providers.semantic._DEPS_OK", True)
|
|
140
140
|
|
|
141
141
|
with patch("fastembed.TextEmbedding", _FakeTextEmbedding):
|
|
@@ -155,7 +155,7 @@ def test_empty_index_safe_null(tmp_path, monkeypatch):
|
|
|
155
155
|
def test_below_floor_returns_none(tmp_path, monkeypatch):
|
|
156
156
|
(tmp_path / "code.py").write_text("x = 1\n")
|
|
157
157
|
db_path = tmp_path / "semantic.db"
|
|
158
|
-
monkeypatch.setattr("codeintel.
|
|
158
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
159
159
|
monkeypatch.setattr("codeintel.providers.semantic._DEPS_OK", True)
|
|
160
160
|
|
|
161
161
|
with patch("fastembed.TextEmbedding", _FakeTextEmbedding), \
|
|
@@ -195,7 +195,7 @@ def test_context_op_is_accepted(monkeypatch):
|
|
|
195
195
|
|
|
196
196
|
def test_provider_never_raises(tmp_path, monkeypatch):
|
|
197
197
|
db_path = tmp_path / "semantic.db"
|
|
198
|
-
monkeypatch.setattr("codeintel.
|
|
198
|
+
monkeypatch.setattr("codeintel.semantic_db._base_dir", lambda: tmp_path)
|
|
199
199
|
monkeypatch.setattr("codeintel.providers.semantic._DEPS_OK", True)
|
|
200
200
|
|
|
201
201
|
with patch("codeintel.semantic_db.SemanticDb.init", side_effect=RuntimeError("injected")):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.2"
|
|
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
|