codecortex 0.14.0__tar.gz → 0.14.2__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.
- {codecortex-0.14.0/src/codecortex.egg-info → codecortex-0.14.2}/PKG-INFO +18 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/README.md +17 -0
- {codecortex-0.14.0 → codecortex-0.14.2/src/codecortex.egg-info}/PKG-INFO +18 -1
- codecortex-0.14.2/src/codeintel/__init__.py +1 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/_common.py +11 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/graph.py +7 -2
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/map.py +5 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/setup.py +7 -2
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/status.py +5 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/gateway.py +20 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/indexer.py +24 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/installer.py +33 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/provider.py +4 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/providers/graph.py +23 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/reindexer.py +24 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/semantic_db.py +5 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_chunking.py +36 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_cli_commands.py +18 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_gateway.py +93 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_graph_provider.py +22 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_installer.py +35 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_reindexer.py +20 -0
- codecortex-0.14.0/src/codeintel/__init__.py +0 -1
- {codecortex-0.14.0 → codecortex-0.14.2}/LICENSE +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/pyproject.toml +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/setup.cfg +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codecortex.egg-info/SOURCES.txt +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/__main__.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/auth.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/cache.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/__init__.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/doctor.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/gen_token.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/index.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/install.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/query.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/reset.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/serve.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/commands/serve_http.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/config.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/doctor.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/grapher.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/http_server.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/injector.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/mapper.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/metrics.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/policy.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/py.typed +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/reset.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/searcher.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/server.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/term.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/verify.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_cache.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_cli_help.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_config.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_doctor.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_e2e.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_enterprise.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_graph_real.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_grapher.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_hardening.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_http_auth.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_http_server.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_integration.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_lsp_real.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_mapper.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_mcp_server.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_model_dimension.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_never_raise.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_onboarding.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_rbac.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_rerank.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_reset.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_term.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_treesitter.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_verify.py +0 -0
- {codecortex-0.14.0 → codecortex-0.14.2}/tests/test_verify_call.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.2
|
|
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
|
|
@@ -439,6 +439,23 @@ docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
|
439
439
|
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
+
## Reporting a problem
|
|
443
|
+
|
|
444
|
+
`codeintel doctor --json` prints a complete, machine-readable picture of what's installed, what's
|
|
445
|
+
runnable, and whether this repo is indexed — per engine, with the remediation for each gap. Paste
|
|
446
|
+
it into an issue and the report is actionable immediately instead of needing a round trip:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
codeintel doctor --json
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
It reports only local engine and index state. Over the HTTP transport the `registrations` field —
|
|
453
|
+
which names agent config files on the machine running the server — is deliberately omitted.
|
|
454
|
+
|
|
455
|
+
If a *result* looks wrong rather than a command failing, include the exact `code.query` call and
|
|
456
|
+
its full envelope. `reason`, `hint`, `engine`, `cached`, and `reindexing` between them explain
|
|
457
|
+
which engine answered and how current its index was, which is usually the whole diagnosis.
|
|
458
|
+
|
|
442
459
|
## Development
|
|
443
460
|
|
|
444
461
|
```bash
|
|
@@ -402,6 +402,23 @@ docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
|
402
402
|
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
403
403
|
```
|
|
404
404
|
|
|
405
|
+
## Reporting a problem
|
|
406
|
+
|
|
407
|
+
`codeintel doctor --json` prints a complete, machine-readable picture of what's installed, what's
|
|
408
|
+
runnable, and whether this repo is indexed — per engine, with the remediation for each gap. Paste
|
|
409
|
+
it into an issue and the report is actionable immediately instead of needing a round trip:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
codeintel doctor --json
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
It reports only local engine and index state. Over the HTTP transport the `registrations` field —
|
|
416
|
+
which names agent config files on the machine running the server — is deliberately omitted.
|
|
417
|
+
|
|
418
|
+
If a *result* looks wrong rather than a command failing, include the exact `code.query` call and
|
|
419
|
+
its full envelope. `reason`, `hint`, `engine`, `cached`, and `reindexing` between them explain
|
|
420
|
+
which engine answered and how current its index was, which is usually the whole diagnosis.
|
|
421
|
+
|
|
405
422
|
## Development
|
|
406
423
|
|
|
407
424
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.2
|
|
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
|
|
@@ -439,6 +439,23 @@ docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
|
439
439
|
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
+
## Reporting a problem
|
|
443
|
+
|
|
444
|
+
`codeintel doctor --json` prints a complete, machine-readable picture of what's installed, what's
|
|
445
|
+
runnable, and whether this repo is indexed — per engine, with the remediation for each gap. Paste
|
|
446
|
+
it into an issue and the report is actionable immediately instead of needing a round trip:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
codeintel doctor --json
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
It reports only local engine and index state. Over the HTTP transport the `registrations` field —
|
|
453
|
+
which names agent config files on the machine running the server — is deliberately omitted.
|
|
454
|
+
|
|
455
|
+
If a *result* looks wrong rather than a command failing, include the exact `code.query` call and
|
|
456
|
+
its full envelope. `reason`, `hint`, `engine`, `cached`, and `reindexing` between them explain
|
|
457
|
+
which engine answered and how current its index was, which is usually the whole diagnosis.
|
|
458
|
+
|
|
442
459
|
## Development
|
|
443
460
|
|
|
444
461
|
```bash
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.14.2"
|
|
@@ -18,6 +18,17 @@ def resolve_root(args: Any) -> str:
|
|
|
18
18
|
return getattr(args, "project_root", None) or os.getcwd()
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def require_dir(project_root: str, command: str) -> str | None:
|
|
22
|
+
"""An error line when *project_root* is not a directory, else None.
|
|
23
|
+
|
|
24
|
+
A mistyped path used to produce confident, well-formed output about a directory that does not
|
|
25
|
+
exist — `setup /typo` rendered a full three-engine health table for it — which is the worst
|
|
26
|
+
possible response to a typo in a script, because it is indistinguishable from success."""
|
|
27
|
+
if os.path.isdir(project_root):
|
|
28
|
+
return None
|
|
29
|
+
return f"{command} failed: not a directory: {project_root}"
|
|
30
|
+
|
|
31
|
+
|
|
21
32
|
def emit(report: dict, *, as_json: bool, render: Callable[[dict], str]) -> None:
|
|
22
33
|
"""Print a report as structured JSON (--json) or as its human-facing text rendering."""
|
|
23
34
|
if as_json:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from codeintel.commands._common import never_raise, resolve_root
|
|
6
|
+
from codeintel.commands._common import never_raise, require_dir, resolve_root
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
# code=1: this command's job is to WRITE A FILE. Exiting 0 after failing to write it
|
|
@@ -12,7 +12,12 @@ from codeintel.commands._common import never_raise, resolve_root
|
|
|
12
12
|
def run(args: Any) -> int:
|
|
13
13
|
from codeintel import grapher
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
project_root = resolve_root(args)
|
|
16
|
+
problem = require_dir(project_root, "graph")
|
|
17
|
+
if problem:
|
|
18
|
+
print(problem)
|
|
19
|
+
return 1
|
|
20
|
+
payload = grapher.build_graph_payload(project_root, limit=args.limit)
|
|
16
21
|
nodes, edges = len(payload.get("nodes", [])), len(payload.get("edges", []))
|
|
17
22
|
|
|
18
23
|
if not args.html:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from codeintel.commands._common import never_raise, resolve_root
|
|
5
|
+
from codeintel.commands._common import never_raise, require_dir, resolve_root
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
# Never-raise parity with the MCP code.map handler — degrade, don't crash.
|
|
@@ -15,6 +15,10 @@ def run(args: Any) -> int:
|
|
|
15
15
|
from codeintel.providers.graph import GraphProvider
|
|
16
16
|
|
|
17
17
|
project_root = resolve_root(args)
|
|
18
|
+
problem = require_dir(project_root, "map")
|
|
19
|
+
if problem:
|
|
20
|
+
print(problem)
|
|
21
|
+
return 1
|
|
18
22
|
try:
|
|
19
23
|
provider = GraphProvider()
|
|
20
24
|
except Exception:
|
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from codeintel.commands._common import emit, never_raise, resolve_root
|
|
5
|
+
from codeintel.commands._common import emit, never_raise, require_dir, resolve_root
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@never_raise("setup unavailable: {exc}")
|
|
9
9
|
def run(args: Any) -> int:
|
|
10
10
|
from codeintel import onboarding
|
|
11
11
|
|
|
12
|
+
project_root = resolve_root(args)
|
|
13
|
+
problem = require_dir(project_root, "setup")
|
|
14
|
+
if problem:
|
|
15
|
+
print(problem)
|
|
16
|
+
return 1
|
|
12
17
|
all_steps = getattr(args, "all_steps", False) # --all implies every automatable step
|
|
13
18
|
report = onboarding.run_setup(
|
|
14
|
-
|
|
19
|
+
project_root,
|
|
15
20
|
install_uv=args.install_uv or all_steps,
|
|
16
21
|
install_deps=args.install_deps or all_steps,
|
|
17
22
|
do_index=args.index or all_steps,
|
|
@@ -4,7 +4,7 @@ import datetime
|
|
|
4
4
|
import os
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from codeintel.commands._common import never_raise, resolve_root
|
|
7
|
+
from codeintel.commands._common import never_raise, require_dir, resolve_root
|
|
8
8
|
|
|
9
9
|
# "available" alone was the misleading word: it meant "a binary is on PATH", which is not the same
|
|
10
10
|
# as runnable, and not the same as usable on THIS repo. Say which.
|
|
@@ -16,6 +16,10 @@ def run(args: Any) -> int:
|
|
|
16
16
|
from codeintel import server
|
|
17
17
|
|
|
18
18
|
project_root = resolve_root(args)
|
|
19
|
+
problem = require_dir(project_root, "status")
|
|
20
|
+
if problem:
|
|
21
|
+
print(problem)
|
|
22
|
+
return 1
|
|
19
23
|
status = server.code_status_handler({"project_root": project_root})
|
|
20
24
|
|
|
21
25
|
readiness = status.get("readiness") or {}
|
|
@@ -40,6 +40,15 @@ _UNCACHED_OPS: frozenset[str] = frozenset({"changed", "changes"})
|
|
|
40
40
|
_ADOPTABLE_ENGINES: frozenset[str] = frozenset({"graph", "lsp", "semantic"})
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def _mark_reindexing(result: Result, reindexing: bool) -> Result:
|
|
44
|
+
"""Flag an answer served while a reindex for its project is still running."""
|
|
45
|
+
if not reindexing or result.get("result") is None:
|
|
46
|
+
return result
|
|
47
|
+
return {**result, "reindexing": True,
|
|
48
|
+
"hint": "a reindex is in progress — this answer reflects the index as of the last "
|
|
49
|
+
"completed pass; re-ask shortly if you have just changed this code"}
|
|
50
|
+
|
|
51
|
+
|
|
43
52
|
class Gateway:
|
|
44
53
|
def __init__(self, graph=None, lsp=None, semantic=None, policy: TieringPolicy | None = None,
|
|
45
54
|
reindexer: Reindexer | None = None):
|
|
@@ -236,6 +245,16 @@ class Gateway:
|
|
|
236
245
|
except Exception:
|
|
237
246
|
pass
|
|
238
247
|
|
|
248
|
+
# If a reindex is running, this answer comes from the PREVIOUS index. Structural
|
|
249
|
+
# answers (callers/impact/hotspots) hash a symbol name, not file bytes, so nothing
|
|
250
|
+
# else in the envelope can reveal that — and an agent that just edited and asked
|
|
251
|
+
# "what did I break?" lands precisely here. Busting the cache would not help: the
|
|
252
|
+
# index itself is behind, so re-asking refetches the same stale data.
|
|
253
|
+
try:
|
|
254
|
+
reindexing = self._reindexer.reindex_pending(str(project_root or ""))
|
|
255
|
+
except Exception:
|
|
256
|
+
reindexing = False
|
|
257
|
+
|
|
239
258
|
# Legacy list-based path (backward compat with pre-Phase-2 tests)
|
|
240
259
|
if self._legacy_providers is not None:
|
|
241
260
|
for p in self._legacy_providers:
|
|
@@ -324,7 +343,7 @@ class Gateway:
|
|
|
324
343
|
|
|
325
344
|
if not uncacheable:
|
|
326
345
|
self._cache.put(op_str, target_str, cache_engine, root_str, result, freshness)
|
|
327
|
-
return result
|
|
346
|
+
return _mark_reindexing(result, reindexing)
|
|
328
347
|
|
|
329
348
|
except Exception as exc:
|
|
330
349
|
log_swallowed("Gateway.query", exc)
|
|
@@ -18,6 +18,10 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
# Generous enough that no hand-written function is affected.
|
|
19
19
|
_MAX_CHUNK_CHARS = 200_000
|
|
20
20
|
|
|
21
|
+
# Bytes examined when deciding whether a file is binary. A NUL in the first block is the classic
|
|
22
|
+
# signal and is what `git` itself uses.
|
|
23
|
+
_BINARY_SNIFF_BYTES = 8192
|
|
24
|
+
|
|
21
25
|
_INDEXED_EXTS = frozenset({
|
|
22
26
|
".py", ".md",
|
|
23
27
|
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", # TS/JS variants
|
|
@@ -108,6 +112,15 @@ def _ts_decl_is_function(node) -> bool:
|
|
|
108
112
|
return False
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
def _looks_binary(path) -> bool:
|
|
116
|
+
"""Whether *path* is binary, by the same rule git uses: a NUL byte in the opening block."""
|
|
117
|
+
try:
|
|
118
|
+
with open(path, "rb") as fh:
|
|
119
|
+
return b"\x00" in fh.read(_BINARY_SNIFF_BYTES)
|
|
120
|
+
except OSError:
|
|
121
|
+
return True # unreadable → treat as unindexable rather than crash
|
|
122
|
+
|
|
123
|
+
|
|
111
124
|
class Indexer:
|
|
112
125
|
def __init__(
|
|
113
126
|
self,
|
|
@@ -210,6 +223,10 @@ class Indexer:
|
|
|
210
223
|
except Exception as exc:
|
|
211
224
|
logger.warning("Cleanup pass failed: %s", exc)
|
|
212
225
|
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _binary_check(path) -> bool:
|
|
228
|
+
return _looks_binary(path)
|
|
229
|
+
|
|
213
230
|
def _walk_files(self, root: Path):
|
|
214
231
|
"""Indexable files inside *root* — and strictly inside it.
|
|
215
232
|
|
|
@@ -259,6 +276,13 @@ class Indexer:
|
|
|
259
276
|
logger.warning("skipping %s — %d hard links, so its content may also live "
|
|
260
277
|
"outside the indexed root", candidate, links)
|
|
261
278
|
continue
|
|
279
|
+
# A source extension is not a promise of source. A compiled artifact or blob named
|
|
280
|
+
# `.py` was read with errors="replace" and embedded as replacement-character
|
|
281
|
+
# garbage — 196KB of /dev/urandom produced 162 chunks — which then competed for
|
|
282
|
+
# rank against real code in every search.
|
|
283
|
+
if _looks_binary(candidate):
|
|
284
|
+
logger.warning("skipping %s — looks binary despite its extension", candidate)
|
|
285
|
+
continue
|
|
262
286
|
yield candidate
|
|
263
287
|
|
|
264
288
|
# ---- chunk-span computation ------------------------------------------------------------
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import pathlib
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
7
8
|
|
|
8
9
|
_AGENTS = ["claude", "codex", "gemini", "zed"]
|
|
@@ -308,10 +309,41 @@ class Installer:
|
|
|
308
309
|
return None
|
|
309
310
|
return None
|
|
310
311
|
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _looks_like_jsonc(text: str) -> bool:
|
|
314
|
+
"""Whether *text* is JSON-with-comments — valid for Zed, invalid for `json.loads`."""
|
|
315
|
+
stripped = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
|
316
|
+
stripped = re.sub(r"(?m)^\s*//.*$", "", stripped)
|
|
317
|
+
stripped = re.sub(r",(\s*[}\]])", r"\1", stripped)
|
|
318
|
+
try:
|
|
319
|
+
json.loads(stripped)
|
|
320
|
+
except Exception:
|
|
321
|
+
return False
|
|
322
|
+
return True
|
|
323
|
+
|
|
311
324
|
def _register_json(self, agent: str, config_path: pathlib.Path, spec: dict,
|
|
312
325
|
command: str) -> dict:
|
|
313
326
|
if config_path.exists():
|
|
314
|
-
|
|
327
|
+
raw = config_path.read_text(encoding="utf-8")
|
|
328
|
+
try:
|
|
329
|
+
data = json.loads(raw)
|
|
330
|
+
except json.JSONDecodeError:
|
|
331
|
+
# Zed ships settings.json as JSONC — comments and trailing commas — so strict
|
|
332
|
+
# parsing fails and every Zed user was blocked with an opaque "Expecting value".
|
|
333
|
+
# Parsing the JSONC and writing back with json.dumps would be WORSE: it would
|
|
334
|
+
# silently delete the user's comments, which in Zed's default config is most of
|
|
335
|
+
# the file. Until this can insert textually (the way the Codex TOML path does),
|
|
336
|
+
# hand the user the exact block instead of damaging their editor config.
|
|
337
|
+
if self._looks_like_jsonc(raw):
|
|
338
|
+
entry = json.dumps({spec["key"][-1] if spec.get("key") else "codeintel":
|
|
339
|
+
launch_value(command)}, indent=2)
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"{config_path} is JSONC (it has comments or trailing commas), which "
|
|
342
|
+
f"codeintel will not rewrite because doing so would discard your "
|
|
343
|
+
f"comments. Add this to \"{'.'.join(spec['key'][:-1]) or 'the root'}\" "
|
|
344
|
+
f"by hand:\n{entry}"
|
|
345
|
+
) from None
|
|
346
|
+
raise
|
|
315
347
|
if not isinstance(data, dict):
|
|
316
348
|
# Every other malformed-input path here fails safe (invalid JSON → failed, no
|
|
317
349
|
# write). Silently substituting {} made this the one branch that DESTROYED the
|
|
@@ -33,6 +33,10 @@ class Result(TypedDict):
|
|
|
33
33
|
cached: bool
|
|
34
34
|
reason: NotRequired[str]
|
|
35
35
|
hint: NotRequired[str]
|
|
36
|
+
# Set when the answer was served while a reindex for its project was still running, i.e. it
|
|
37
|
+
# reflects the last COMPLETED index rather than the current source. Optional, and the MCP
|
|
38
|
+
# tools deliberately return plain `dict` so this never becomes a required schema field.
|
|
39
|
+
reindexing: NotRequired[bool]
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
@runtime_checkable
|
|
@@ -26,6 +26,28 @@ _GRAPH_OPS = frozenset({
|
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _strip_project_prefix(qualified_name: str) -> str:
|
|
30
|
+
"""Drop the backend's project id from the head of a qualified name.
|
|
31
|
+
|
|
32
|
+
The backend prefixes every qualified name with its own project id, which for a path-slug
|
|
33
|
+
registration is the flattened absolute path — so each result line began
|
|
34
|
+
`Users-alice-Documents-project-myrepo.src.pkg.fn`. That is the author's home directory
|
|
35
|
+
layout repeated on every row: noise for a human, wasted tokens for the agent this tool
|
|
36
|
+
exists to serve, on results that can run to a hundred lines.
|
|
37
|
+
|
|
38
|
+
Only a leading path-slug-looking segment is removed. A qualified name that starts with a
|
|
39
|
+
real module (`src.codeintel.gateway.query`) is left exactly as it is.
|
|
40
|
+
"""
|
|
41
|
+
head, sep, rest = qualified_name.partition(".")
|
|
42
|
+
if not sep:
|
|
43
|
+
return qualified_name
|
|
44
|
+
# A slug: no spaces, and hyphenated (the backend joins path components with "-"). A genuine
|
|
45
|
+
# Python package name cannot contain a hyphen, so this cannot eat a real module.
|
|
46
|
+
if "-" in head and " " not in head:
|
|
47
|
+
return rest
|
|
48
|
+
return qualified_name
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
def _repo_display_name(root: str) -> str:
|
|
30
52
|
"""The repo's own directory name, for headings a human will read.
|
|
31
53
|
|
|
@@ -276,7 +298,7 @@ class GraphProvider:
|
|
|
276
298
|
@staticmethod
|
|
277
299
|
def _display(row: dict, name_key: str, qn_key: str, file_key: str) -> str:
|
|
278
300
|
name = str(row.get(name_key) or "?")
|
|
279
|
-
qn = str(row.get(qn_key) or "")
|
|
301
|
+
qn = _strip_project_prefix(str(row.get(qn_key) or ""))
|
|
280
302
|
file = str(row.get(file_key) or "")
|
|
281
303
|
edge = str(row.get("type(c)") or "").strip()
|
|
282
304
|
label = qn or name
|
|
@@ -52,12 +52,27 @@ class Reindexer:
|
|
|
52
52
|
# folds it into the cache key so a structural answer is invalidated once the
|
|
53
53
|
# index actually moves (a symbol/free-text target has no file content to hash).
|
|
54
54
|
self._generation: dict[str, int] = {}
|
|
55
|
+
# Roots with a reindex submitted but not yet finished. The index is KNOWN to be behind
|
|
56
|
+
# while this holds, and no amount of cache invalidation fixes that — re-asking just
|
|
57
|
+
# refetches the same stale index, more expensively. The only honest response is to say so.
|
|
58
|
+
self._in_flight: set[str] = set()
|
|
55
59
|
self._executor = _DaemonPool(max_workers=2)
|
|
56
60
|
|
|
57
61
|
def generation(self, project_root: str) -> int:
|
|
58
62
|
with self._lock:
|
|
59
63
|
return self._generation.get(project_root, 0)
|
|
60
64
|
|
|
65
|
+
def reindex_pending(self, project_root: str) -> bool:
|
|
66
|
+
"""Whether this root has a reindex running right now.
|
|
67
|
+
|
|
68
|
+
Lets a caller distinguish "this is the current structure" from "this is the structure as
|
|
69
|
+
of the last completed index, and a newer one is being built". An agent's loop is edit →
|
|
70
|
+
ask what I broke, which lands exactly in that window."""
|
|
71
|
+
if not project_root:
|
|
72
|
+
return False
|
|
73
|
+
with self._lock:
|
|
74
|
+
return project_root in self._in_flight
|
|
75
|
+
|
|
61
76
|
def maybe_reindex(self, project_root: str) -> None:
|
|
62
77
|
if not self._enabled:
|
|
63
78
|
return
|
|
@@ -66,6 +81,12 @@ class Reindexer:
|
|
|
66
81
|
|
|
67
82
|
now = time.monotonic()
|
|
68
83
|
with self._lock:
|
|
84
|
+
# Already running: skip. The debounce timestamp is set when a pass is SUBMITTED, not
|
|
85
|
+
# when it finishes, so on a repo whose reindex outlasts the window every later query
|
|
86
|
+
# stacked another concurrent pass — overlapping writers against one SQLite file and
|
|
87
|
+
# one graph subprocess, for no benefit.
|
|
88
|
+
if project_root in self._in_flight:
|
|
89
|
+
return
|
|
69
90
|
last = self._last_fired.get(project_root, 0.0)
|
|
70
91
|
if now - last < self._debounce_seconds:
|
|
71
92
|
return
|
|
@@ -77,6 +98,8 @@ class Reindexer:
|
|
|
77
98
|
if self._reindex_disabled(project_root):
|
|
78
99
|
return
|
|
79
100
|
|
|
101
|
+
with self._lock:
|
|
102
|
+
self._in_flight.add(project_root)
|
|
80
103
|
self._executor.submit(self._do_reindex, project_root)
|
|
81
104
|
|
|
82
105
|
def _reindex_disabled(self, project_root: str) -> bool:
|
|
@@ -113,6 +136,7 @@ class Reindexer:
|
|
|
113
136
|
finally:
|
|
114
137
|
with self._lock:
|
|
115
138
|
self._generation[project_root] = self._generation.get(project_root, 0) + 1
|
|
139
|
+
self._in_flight.discard(project_root)
|
|
116
140
|
|
|
117
141
|
def _semantic_reindex(self, project_root: str) -> None:
|
|
118
142
|
import pathlib
|
|
@@ -149,7 +149,11 @@ class SemanticDb:
|
|
|
149
149
|
if self.dimension is not None:
|
|
150
150
|
logger.warning(
|
|
151
151
|
"embedding dimension %d != cache dimension %d — skipping write; run "
|
|
152
|
-
|
|
152
|
+
# `reset` alone cannot fix this: the vec0 table's dimension is fixed at
|
|
153
|
+
# creation and the table is SHARED across every project in this cache file,
|
|
154
|
+
# so a project-scoped reset (which only DELETEs that project's rows) leaves
|
|
155
|
+
# it in place and the warning repeats forever. `--all` drops the file.
|
|
156
|
+
"`codeintel reset --all` to rebuild the semantic index for the new model",
|
|
153
157
|
dim, self.dimension,
|
|
154
158
|
)
|
|
155
159
|
return None
|
|
@@ -363,3 +363,39 @@ def test_switching_strategy_reconciles_stale_line_chunks(tmp_path):
|
|
|
363
363
|
|
|
364
364
|
# 25-line def > max_chunk_lines(40)? no — 25 <= 40, so it becomes ONE def chunk at line 0.
|
|
365
365
|
assert syntax_starts == {0}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_a_binary_file_with_a_source_extension_is_not_indexed(tmp_path):
|
|
369
|
+
"""A source extension is not a promise of source. A compiled artifact named `.py` was read
|
|
370
|
+
with errors="replace" and embedded as replacement-character garbage — 196KB of random bytes
|
|
371
|
+
produced 162 chunks — which then competed for rank against real code in every search."""
|
|
372
|
+
from codeintel.indexer import Indexer
|
|
373
|
+
from codeintel.semantic_db import SemanticDb
|
|
374
|
+
|
|
375
|
+
(tmp_path / "real.py").write_text("def hello():\n return 1\n")
|
|
376
|
+
(tmp_path / "blob.py").write_bytes(b"\x7fELF\x00\x00\x00" + os.urandom(4096))
|
|
377
|
+
|
|
378
|
+
db = SemanticDb(str(tmp_path / "db.sqlite"))
|
|
379
|
+
db.init()
|
|
380
|
+
try:
|
|
381
|
+
names = sorted(f.name for f in Indexer(db)._walk_files(tmp_path))
|
|
382
|
+
finally:
|
|
383
|
+
db.close()
|
|
384
|
+
|
|
385
|
+
assert names == ["real.py"], f"a binary file was queued for embedding: {names}"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_utf8_source_with_unusual_characters_is_still_indexed(tmp_path):
|
|
389
|
+
"""Guard the guard: the NUL sniff must not reject legitimate non-ASCII source."""
|
|
390
|
+
from codeintel.indexer import Indexer
|
|
391
|
+
from codeintel.semantic_db import SemanticDb
|
|
392
|
+
|
|
393
|
+
(tmp_path / "unicode.py").write_text("# ünïcödé ✓ 日本語\ndef f():\n return '→'\n",
|
|
394
|
+
encoding="utf-8")
|
|
395
|
+
db = SemanticDb(str(tmp_path / "db.sqlite"))
|
|
396
|
+
db.init()
|
|
397
|
+
try:
|
|
398
|
+
names = sorted(f.name for f in Indexer(db)._walk_files(tmp_path))
|
|
399
|
+
finally:
|
|
400
|
+
db.close()
|
|
401
|
+
assert names == ["unicode.py"]
|
|
@@ -792,3 +792,21 @@ def test_index_reports_an_unrecoverable_indexer_failure(monkeypatch, tmp_path, c
|
|
|
792
792
|
assert import_module("codeintel.commands.index").run(_args(project_root=str(tmp_path))) == 1
|
|
793
793
|
out = capsys.readouterr().out
|
|
794
794
|
assert "index failed" in out and "Nothing new to index" not in out
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# --------------------------------------------------------------------------- root validation
|
|
798
|
+
|
|
799
|
+
@pytest.mark.parametrize("command,args_", [
|
|
800
|
+
("setup", {"project_root": None, "all_steps": False, "install_uv": False,
|
|
801
|
+
"install_deps": False, "index": False, "warm": False, "json": False}),
|
|
802
|
+
("status", {"project_root": None}),
|
|
803
|
+
("map", {"project_root": None, "inject": False, "budget": 32768}),
|
|
804
|
+
("graph", {"project_root": None, "html": False, "out": None, "limit": 220}),
|
|
805
|
+
])
|
|
806
|
+
def test_a_nonexistent_project_root_fails_loudly(command, args_, tmp_path, capsys):
|
|
807
|
+
"""A mistyped path produced confident, well-formed output about a directory that does not
|
|
808
|
+
exist — `setup /typo` rendered a full three-engine health table for it. In a script that is
|
|
809
|
+
indistinguishable from success, which is the worst possible response to a typo."""
|
|
810
|
+
args_ = {**args_, "project_root": str(tmp_path / "does-not-exist")}
|
|
811
|
+
assert import_module(f"codeintel.commands.{command}").run(_args(args_)) == 1
|
|
812
|
+
assert "not a directory" in capsys.readouterr().out
|
|
@@ -409,3 +409,96 @@ def test_cache_get_and_put_always_use_the_same_key():
|
|
|
409
409
|
|
|
410
410
|
keys = {engine_arg(c) for c in gets} | {engine_arg(c) for c in puts}
|
|
411
411
|
assert keys == {"cache_engine"}, f"cache key components disagree: {keys}"
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# --------------------------------------------------------------------------- staleness signalling
|
|
415
|
+
|
|
416
|
+
class _Answering:
|
|
417
|
+
available = True
|
|
418
|
+
|
|
419
|
+
def build_result(self, op, target, files, budget, project_root):
|
|
420
|
+
return {"ok": True, "op": op, "target": target, "result": "ANSWER",
|
|
421
|
+
"engine": "graph", "cached": False}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_an_answer_served_during_a_reindex_says_so():
|
|
425
|
+
"""Structural answers hash a symbol NAME, not file bytes, so nothing else in the envelope can
|
|
426
|
+
reveal that the index is behind. An agent's loop is edit -> ask what I broke, which lands
|
|
427
|
+
exactly in that window. Invalidating the cache would not help: the index itself is stale, so
|
|
428
|
+
re-asking just refetches the same data more expensively. Saying so is the only honest fix."""
|
|
429
|
+
from codeintel.gateway import Gateway
|
|
430
|
+
from codeintel.reindexer import Reindexer
|
|
431
|
+
|
|
432
|
+
rx = Reindexer()
|
|
433
|
+
gw = Gateway(graph=_Answering(), lsp=None, semantic=None, reindexer=rx)
|
|
434
|
+
rx._in_flight.add("/repo")
|
|
435
|
+
|
|
436
|
+
res = gw.query(op="callers", target="f", role="", project_root="/repo")
|
|
437
|
+
assert res["result"] == "ANSWER" # still answers — this is a caveat, not a error
|
|
438
|
+
assert res["reindexing"] is True
|
|
439
|
+
assert "re-ask" in res["hint"]
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_a_settled_index_does_not_flag_an_answer():
|
|
443
|
+
"""The signal must be rare enough to mean something — flagging every answer would train a
|
|
444
|
+
reader to ignore it. Steady state is: a reindex ran, the debounce window has not elapsed, so
|
|
445
|
+
no new pass fires and answers come back unqualified. (The FIRST query for a root does fire one
|
|
446
|
+
and is legitimately flagged while it runs — the index really is being rebuilt underneath.)"""
|
|
447
|
+
import time
|
|
448
|
+
|
|
449
|
+
from codeintel.gateway import Gateway
|
|
450
|
+
from codeintel.reindexer import Reindexer
|
|
451
|
+
|
|
452
|
+
rx = Reindexer()
|
|
453
|
+
rx._last_fired["/repo"] = time.monotonic() # inside the debounce window: nothing re-fires
|
|
454
|
+
gw = Gateway(graph=_Answering(), lsp=None, semantic=None, reindexer=rx)
|
|
455
|
+
|
|
456
|
+
res = gw.query(op="callers", target="f", role="", project_root="/repo")
|
|
457
|
+
assert res["result"] == "ANSWER"
|
|
458
|
+
assert res.get("reindexing") is None
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def test_an_empty_result_is_not_flagged_as_reindexing():
|
|
462
|
+
"""`reindexing` qualifies an answer. On a safe-null there is no answer to qualify, and the
|
|
463
|
+
`reason`/`hint` already carry the explanation."""
|
|
464
|
+
from codeintel.gateway import Gateway
|
|
465
|
+
from codeintel.provider import safe_null_result
|
|
466
|
+
from codeintel.reindexer import Reindexer
|
|
467
|
+
|
|
468
|
+
class _Empty(_Answering):
|
|
469
|
+
def build_result(self, op, target, files, budget, project_root):
|
|
470
|
+
return safe_null_result(op, target, engine="graph", reason="not-in-graph")
|
|
471
|
+
|
|
472
|
+
rx = Reindexer()
|
|
473
|
+
gw = Gateway(graph=_Empty(), lsp=None, semantic=None, reindexer=rx)
|
|
474
|
+
rx._in_flight.add("/repo")
|
|
475
|
+
|
|
476
|
+
res = gw.query(op="callers", target="f", role="", project_root="/repo")
|
|
477
|
+
assert res.get("reindexing") is None
|
|
478
|
+
assert res["reason"] == "not-in-graph"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def test_reindex_pending_clears_once_the_pass_completes(monkeypatch):
|
|
482
|
+
from codeintel.reindexer import Reindexer
|
|
483
|
+
|
|
484
|
+
rx = Reindexer()
|
|
485
|
+
monkeypatch.setattr(rx, "_semantic_reindex", lambda root: None)
|
|
486
|
+
monkeypatch.setattr(rx, "_graph_reindex", lambda root: None)
|
|
487
|
+
|
|
488
|
+
rx._in_flight.add("/repo")
|
|
489
|
+
assert rx.reindex_pending("/repo") is True
|
|
490
|
+
rx._do_reindex("/repo")
|
|
491
|
+
assert rx.reindex_pending("/repo") is False, "a completed reindex must clear the flag"
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_reindex_pending_clears_even_when_both_passes_fail(monkeypatch):
|
|
495
|
+
"""A stuck flag would mark every later answer stale and train the reader to ignore it."""
|
|
496
|
+
from codeintel.reindexer import Reindexer
|
|
497
|
+
|
|
498
|
+
rx = Reindexer()
|
|
499
|
+
for name in ("_semantic_reindex", "_graph_reindex"):
|
|
500
|
+
monkeypatch.setattr(rx, name, lambda root: (_ for _ in ()).throw(RuntimeError("down")))
|
|
501
|
+
|
|
502
|
+
rx._in_flight.add("/repo")
|
|
503
|
+
rx._do_reindex("/repo")
|
|
504
|
+
assert rx.reindex_pending("/repo") is False
|
|
@@ -308,3 +308,25 @@ def test_overview_falls_back_to_the_backend_name_without_a_root(monkeypatch):
|
|
|
308
308
|
"project": "backend-id", "total_nodes": 5, "total_edges": 4})
|
|
309
309
|
|
|
310
310
|
assert gp._op_overview("", "backend-id", 1000, "").splitlines()[0] == "## Architecture: backend-id"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_result_lines_do_not_carry_the_backends_project_id(tmp_path):
|
|
314
|
+
"""The backend prefixes every qualified name with its project id — for a path-slug
|
|
315
|
+
registration, the flattened ABSOLUTE PATH. Every result line therefore began
|
|
316
|
+
`Users-alice-Documents-project-myrepo.src.pkg.fn`: the author's home directory repeated per
|
|
317
|
+
row, on results that run to a hundred lines. Noise for a human, wasted tokens for the agent
|
|
318
|
+
this tool exists to serve."""
|
|
319
|
+
from codeintel.providers.graph import _strip_project_prefix
|
|
320
|
+
|
|
321
|
+
assert _strip_project_prefix(
|
|
322
|
+
"Users-alice-Documents-project-myrepo.src.pkg.fn") == "src.pkg.fn"
|
|
323
|
+
assert _strip_project_prefix("my-repo.src.pkg.fn") == "src.pkg.fn"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_stripping_never_eats_a_real_module_path():
|
|
327
|
+
"""A hyphen cannot appear in a Python package name, which is what makes the slug detectable —
|
|
328
|
+
so a genuine dotted module path must pass through untouched."""
|
|
329
|
+
from codeintel.providers.graph import _strip_project_prefix
|
|
330
|
+
|
|
331
|
+
for qn in ("src.codeintel.gateway.query", "codeintel.server.run", "main", "", "pkg.mod"):
|
|
332
|
+
assert _strip_project_prefix(qn) == qn
|
|
@@ -561,3 +561,38 @@ def test_install_refuses_to_overwrite_a_non_object_config(tmp_path, monkeypatch)
|
|
|
561
561
|
|
|
562
562
|
assert result["ok"] is False
|
|
563
563
|
assert cfg.read_text() == "[1, 2, 3]\n", "the user's file was overwritten"
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def test_zed_jsonc_config_is_not_rewritten_and_the_error_is_actionable(tmp_path, monkeypatch):
|
|
567
|
+
"""Zed ships settings.json as JSONC — comments and trailing commas — so strict parsing failed
|
|
568
|
+
and every Zed user was blocked by an opaque "Expecting value". Parsing the JSONC and writing
|
|
569
|
+
back with json.dumps would be WORSE: it would silently delete the user's comments, which in
|
|
570
|
+
Zed's default config is most of the file. Fail, keep the file intact, and hand over the block.
|
|
571
|
+
"""
|
|
572
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
573
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
|
574
|
+
cfg = tmp_path / "xdg" / "zed" / "settings.json"
|
|
575
|
+
cfg.parent.mkdir(parents=True)
|
|
576
|
+
original = '{\n // Font settings\n "buffer_font_size": 14,\n "theme": "One Dark",\n}\n'
|
|
577
|
+
cfg.write_text(original)
|
|
578
|
+
|
|
579
|
+
res = Installer().register("zed")
|
|
580
|
+
|
|
581
|
+
assert res["ok"] is False
|
|
582
|
+
assert cfg.read_text() == original, "the user's editor config was modified"
|
|
583
|
+
assert "JSONC" in res["reason"] and "context_servers" in res["reason"]
|
|
584
|
+
assert "codeintel" in res["reason"] # the block to paste is in the message
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def test_plain_invalid_json_still_reports_the_parse_error(tmp_path, monkeypatch):
|
|
588
|
+
"""Only genuine JSONC gets the special path — a truly corrupt file must not be misdiagnosed."""
|
|
589
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
590
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path))
|
|
591
|
+
cfg = tmp_path / ".claude.json"
|
|
592
|
+
cfg.write_text("{ this is not json at all ")
|
|
593
|
+
|
|
594
|
+
res = Installer().register("claude")
|
|
595
|
+
|
|
596
|
+
assert res["ok"] is False
|
|
597
|
+
assert "JSONC" not in res["reason"]
|
|
598
|
+
assert cfg.read_text() == "{ this is not json at all "
|
|
@@ -260,3 +260,23 @@ def test_repeated_failures_keep_advancing_rather_than_pinning_at_zero(monkeypatc
|
|
|
260
260
|
for _ in range(3):
|
|
261
261
|
r._do_reindex("/repo")
|
|
262
262
|
assert r.generation("/repo") == 3
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_a_reindex_already_running_is_not_submitted_again(monkeypatch):
|
|
266
|
+
"""The debounce timestamp is set when a pass is SUBMITTED, not when it finishes, so on a repo
|
|
267
|
+
whose reindex outlasts the window every later query stacked another concurrent pass —
|
|
268
|
+
overlapping writers against one SQLite file and one graph subprocess, for no benefit."""
|
|
269
|
+
r = Reindexer(debounce_seconds=0)
|
|
270
|
+
submitted = []
|
|
271
|
+
monkeypatch.setattr(r._executor, "submit", lambda fn, root: submitted.append(root))
|
|
272
|
+
|
|
273
|
+
r.maybe_reindex("/repo")
|
|
274
|
+
assert submitted == ["/repo"]
|
|
275
|
+
|
|
276
|
+
r.maybe_reindex("/repo") # still in flight (nothing cleared it)
|
|
277
|
+
r.maybe_reindex("/repo")
|
|
278
|
+
assert submitted == ["/repo"], "a second pass was stacked on a running one"
|
|
279
|
+
|
|
280
|
+
r._in_flight.discard("/repo") # the running pass completes
|
|
281
|
+
r.maybe_reindex("/repo")
|
|
282
|
+
assert submitted == ["/repo", "/repo"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.14.0"
|
|
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
|