codecortex 0.15.2__tar.gz → 0.15.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.
- {codecortex-0.15.2/src/codecortex.egg-info → codecortex-0.15.4}/PKG-INFO +1 -1
- {codecortex-0.15.2 → codecortex-0.15.4/src/codecortex.egg-info}/PKG-INFO +1 -1
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codecortex.egg-info/SOURCES.txt +8 -0
- codecortex-0.15.4/src/codeintel/__init__.py +1 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/query.py +35 -1
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/gateway.py +70 -11
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/indexer.py +5 -6
- codecortex-0.15.4/src/codeintel/loc.py +73 -0
- codecortex-0.15.4/src/codeintel/outcome.py +77 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/provider.py +40 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/providers/graph.py +376 -21
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/providers/lsp.py +241 -37
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/providers/semantic.py +50 -5
- codecortex-0.15.4/src/codeintel/redact.py +113 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/reset.py +63 -2
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/server.py +34 -10
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/source_kind.py +69 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_cli_commands.py +9 -9
- codecortex-0.15.4/tests/test_cold_process.py +532 -0
- codecortex-0.15.4/tests/test_corpus.py +335 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_gateway.py +3 -2
- codecortex-0.15.4/tests/test_graph_failure_population.py +206 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_graph_provider.py +1 -1
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_graph_real.py +67 -1
- codecortex-0.15.4/tests/test_incompleteness.py +580 -0
- codecortex-0.15.4/tests/test_loc_census.py +212 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_lsp_provider.py +126 -3
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_lsp_real.py +38 -19
- codecortex-0.15.2/src/codeintel/__init__.py +0 -1
- {codecortex-0.15.2 → codecortex-0.15.4}/LICENSE +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/README.md +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/pyproject.toml +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/setup.cfg +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/__main__.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/auth.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/cache.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/__init__.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/_common.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/doctor.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/gen_token.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/graph.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/index.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/install.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/map.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/reset.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/serve.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/serve_http.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/setup.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/commands/status.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/config.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/containment.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/doctor.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/grapher.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/http_server.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/injector.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/installer.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/mapper.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/metrics.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/policy.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/py.typed +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/searcher.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/term.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/verify.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_cache.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_chunking.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_cli_help.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_config.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_doctor.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_e2e.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_enterprise.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_grapher.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_hardening.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_http_auth.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_http_server.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_installer.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_integration.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_mapper.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_mcp_server.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_model_dimension.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_never_raise.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_onboarding.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_rbac.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_reindexer.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_rerank.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_reset.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_source_kind.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_term.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_treesitter.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_verify.py +0 -0
- {codecortex-0.15.2 → codecortex-0.15.4}/tests/test_verify_call.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.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.15.
|
|
3
|
+
Version: 0.15.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
|
|
@@ -20,13 +20,16 @@ src/codeintel/http_server.py
|
|
|
20
20
|
src/codeintel/indexer.py
|
|
21
21
|
src/codeintel/injector.py
|
|
22
22
|
src/codeintel/installer.py
|
|
23
|
+
src/codeintel/loc.py
|
|
23
24
|
src/codeintel/logconfig.py
|
|
24
25
|
src/codeintel/mapper.py
|
|
25
26
|
src/codeintel/metrics.py
|
|
26
27
|
src/codeintel/onboarding.py
|
|
28
|
+
src/codeintel/outcome.py
|
|
27
29
|
src/codeintel/policy.py
|
|
28
30
|
src/codeintel/provider.py
|
|
29
31
|
src/codeintel/py.typed
|
|
32
|
+
src/codeintel/redact.py
|
|
30
33
|
src/codeintel/reindexer.py
|
|
31
34
|
src/codeintel/reset.py
|
|
32
35
|
src/codeintel/searcher.py
|
|
@@ -60,12 +63,15 @@ tests/test_cache.py
|
|
|
60
63
|
tests/test_chunking.py
|
|
61
64
|
tests/test_cli_commands.py
|
|
62
65
|
tests/test_cli_help.py
|
|
66
|
+
tests/test_cold_process.py
|
|
63
67
|
tests/test_config.py
|
|
68
|
+
tests/test_corpus.py
|
|
64
69
|
tests/test_doctor.py
|
|
65
70
|
tests/test_e2e.py
|
|
66
71
|
tests/test_engine_adoption.py
|
|
67
72
|
tests/test_enterprise.py
|
|
68
73
|
tests/test_gateway.py
|
|
74
|
+
tests/test_graph_failure_population.py
|
|
69
75
|
tests/test_graph_provider.py
|
|
70
76
|
tests/test_graph_real.py
|
|
71
77
|
tests/test_graph_stdin.py
|
|
@@ -73,8 +79,10 @@ tests/test_grapher.py
|
|
|
73
79
|
tests/test_hardening.py
|
|
74
80
|
tests/test_http_auth.py
|
|
75
81
|
tests/test_http_server.py
|
|
82
|
+
tests/test_incompleteness.py
|
|
76
83
|
tests/test_installer.py
|
|
77
84
|
tests/test_integration.py
|
|
85
|
+
tests/test_loc_census.py
|
|
78
86
|
tests/test_lsp_provider.py
|
|
79
87
|
tests/test_lsp_real.py
|
|
80
88
|
tests/test_mapper.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.15.4"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""`codeintel query` — one question against the gateway: search, callers, callees, impact, chain."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
import time
|
|
6
7
|
from typing import Any
|
|
@@ -12,6 +13,36 @@ from codeintel.provider import Result, safe_null_result
|
|
|
12
13
|
# and reporting whatever the gateway last said.
|
|
13
14
|
_WARMING_TIMEOUT_S = 45.0
|
|
14
15
|
|
|
16
|
+
# Per-query time budget, in milliseconds, handed to whichever engine answers.
|
|
17
|
+
#
|
|
18
|
+
# This used to be omitted entirely, so every engine fell back to its own default — 5s for the LSP
|
|
19
|
+
# provider — against a cold first `symbol` query measured at 11.65s on a real 841-file TypeScript
|
|
20
|
+
# repo. The call timed out, the reference lookup came back empty, and the empty list was rendered
|
|
21
|
+
# as "(none)": a confident false answer produced by a missing argument. A CLI invocation is a
|
|
22
|
+
# human or an agent waiting on one question; it can afford to wait properly.
|
|
23
|
+
_CLI_BUDGET_MS = 30_000
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _budget_ms() -> int:
|
|
27
|
+
"""The per-query budget, overridable via ``CODEINTEL_BUDGET_MS``.
|
|
28
|
+
|
|
29
|
+
Two reasons this is an env var rather than a constant. Operators on slow machines or huge
|
|
30
|
+
repositories need to raise it. And tests need to LOWER it: the cold-process tier exists to catch
|
|
31
|
+
the defect where a timed-out backend call is rendered as a confident "(none)", but on a fast
|
|
32
|
+
machine the cold call simply succeeds, so the tier passed with that exact regression planted
|
|
33
|
+
back in. A budget it can drive to near-zero lets it reproduce the starvation condition
|
|
34
|
+
deterministically instead of waiting for a slow day.
|
|
35
|
+
"""
|
|
36
|
+
raw = os.environ.get("CODEINTEL_BUDGET_MS", "").strip()
|
|
37
|
+
if raw:
|
|
38
|
+
try:
|
|
39
|
+
n = int(raw)
|
|
40
|
+
if n > 0:
|
|
41
|
+
return n
|
|
42
|
+
except ValueError:
|
|
43
|
+
pass
|
|
44
|
+
return _CLI_BUDGET_MS
|
|
45
|
+
|
|
15
46
|
|
|
16
47
|
def run(args: Any) -> int:
|
|
17
48
|
"""`--json` promises parseable stdout, so its failures must be JSON too.
|
|
@@ -55,7 +86,9 @@ def _query(args: Any) -> Result:
|
|
|
55
86
|
|
|
56
87
|
project_root = resolve_root(args)
|
|
57
88
|
engine = args.engine if args.engine != "auto" else None
|
|
58
|
-
|
|
89
|
+
# oneshot: this process exits when the query returns, so it must not start a background
|
|
90
|
+
# reindex it cannot finish (and must not then report that reindex as staleness).
|
|
91
|
+
gw = server._build_gateway(oneshot=True)
|
|
59
92
|
|
|
60
93
|
def _run_query() -> Result:
|
|
61
94
|
return gw.query(
|
|
@@ -63,6 +96,7 @@ def _query(args: Any) -> Result:
|
|
|
63
96
|
target=args.target,
|
|
64
97
|
engine=engine,
|
|
65
98
|
role="",
|
|
99
|
+
budget=_budget_ms(),
|
|
66
100
|
project_root=project_root,
|
|
67
101
|
)
|
|
68
102
|
|
|
@@ -6,8 +6,9 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from codeintel.cache import ContentHashCache
|
|
8
8
|
from codeintel.policy import TieringPolicy
|
|
9
|
-
from codeintel.provider import Result, log_swallowed, safe_null_result
|
|
9
|
+
from codeintel.provider import Result, attach_confidence, log_swallowed, safe_null_result
|
|
10
10
|
from codeintel.providers.none import NoneProvider
|
|
11
|
+
from codeintel.redact import redact
|
|
11
12
|
from codeintel.reindexer import Reindexer
|
|
12
13
|
|
|
13
14
|
_KNOWN_ENGINES: frozenset[str] = frozenset({"graph", "lsp", "semantic", "auto", "both", "all"})
|
|
@@ -51,7 +52,17 @@ def _mark_reindexing(result: Result, reindexing: bool) -> Result:
|
|
|
51
52
|
|
|
52
53
|
class Gateway:
|
|
53
54
|
def __init__(self, graph=None, lsp=None, semantic=None, policy: TieringPolicy | None = None,
|
|
54
|
-
reindexer: Reindexer | None = None):
|
|
55
|
+
reindexer: Reindexer | None = None, oneshot: bool = False):
|
|
56
|
+
# A one-shot process (the `codeintel` CLI) must not run the long-lived server's background
|
|
57
|
+
# machinery. It used to: every query called `maybe_reindex`, which in a fresh process always
|
|
58
|
+
# passed the debounce (`_last_fired` starts empty) and submitted a pass to a DAEMON pool —
|
|
59
|
+
# then the same query asked `reindex_pending` ten lines later and was told "yes", by itself.
|
|
60
|
+
# That is why `reindexing: true` accompanied literally every answer this tool has ever
|
|
61
|
+
# produced, and why the tree was re-walked on every query. Worse, a daemon thread is killed
|
|
62
|
+
# wherever it happens to be when the process exits, so those passes never completed and wrote
|
|
63
|
+
# torn state on the way out — the most plausible source of the `.corrupt` index files found
|
|
64
|
+
# in the cache. A process that cannot finish a reindex must not start one.
|
|
65
|
+
self._oneshot = bool(oneshot)
|
|
55
66
|
# Backward-compat: old tests pass a list as the first positional arg.
|
|
56
67
|
if isinstance(graph, list):
|
|
57
68
|
self._legacy_providers: list | None = graph
|
|
@@ -179,14 +190,32 @@ class Gateway:
|
|
|
179
190
|
if all_unreachable else "")),
|
|
180
191
|
)
|
|
181
192
|
|
|
182
|
-
|
|
193
|
+
# A fan-out answer is only as whole as its parts. This used to hand-build a six-key envelope
|
|
194
|
+
# and drop both `confidence` and `gaps` on the floor — so a `context` request (the DEFAULT
|
|
195
|
+
# fan-out op) whose graph half timed out returned the lsp half alone, unqualified, and a
|
|
196
|
+
# `partial` a provider had explicitly produced was destroyed on the way out. Worse, an engine
|
|
197
|
+
# that answered NOTHING is silently absent from `parts`: the body simply does not mention it,
|
|
198
|
+
# which reads as "that engine had nothing to add" rather than "that engine could not be asked".
|
|
199
|
+
merged_gaps: list[dict] = []
|
|
200
|
+
for eng, r in results.items():
|
|
201
|
+
merged_gaps.extend({**g, "engine": eng}
|
|
202
|
+
for g in (r.get("gaps") or []) if isinstance(g, dict))
|
|
203
|
+
if r.get("result") is None:
|
|
204
|
+
merged_gaps.append({
|
|
205
|
+
"section": eng,
|
|
206
|
+
"kind": str(r.get("reason") or "no-result"),
|
|
207
|
+
"detail": f"the {eng} engine contributed nothing to this answer "
|
|
208
|
+
f"({r.get('reason') or 'no-result'})",
|
|
209
|
+
"engine": eng,
|
|
210
|
+
})
|
|
211
|
+
return attach_confidence({
|
|
183
212
|
"ok": True,
|
|
184
213
|
"op": op_str,
|
|
185
214
|
"target": target_str,
|
|
186
215
|
"result": "\n\n".join(parts),
|
|
187
216
|
"engine": engine_str,
|
|
188
217
|
"cached": False,
|
|
189
|
-
}
|
|
218
|
+
}, merged_gaps)
|
|
190
219
|
|
|
191
220
|
def _dispatch_single(
|
|
192
221
|
self,
|
|
@@ -235,6 +264,28 @@ class Gateway:
|
|
|
235
264
|
role: str = "",
|
|
236
265
|
budget=None,
|
|
237
266
|
project_root=None,
|
|
267
|
+
) -> Result:
|
|
268
|
+
"""Answer one question. Never raises.
|
|
269
|
+
|
|
270
|
+
A thin wrapper over `_query`, existing so that redaction has exactly ONE seam to cover.
|
|
271
|
+
Every leak found in the evaluation was in a field some renderer built and no one swept —
|
|
272
|
+
the scope note inside `result`, the "index it standalone with:" command inside `hint`. Both
|
|
273
|
+
are downstream of here, and so is anything added later."""
|
|
274
|
+
result = self._query(op, target, engine, role, budget, project_root)
|
|
275
|
+
try:
|
|
276
|
+
return redact(result) # type: ignore[return-value]
|
|
277
|
+
except Exception as exc:
|
|
278
|
+
log_swallowed("Gateway.query.redact", exc)
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
def _query(
|
|
282
|
+
self,
|
|
283
|
+
op=None,
|
|
284
|
+
target=None,
|
|
285
|
+
engine=None,
|
|
286
|
+
role: str = "",
|
|
287
|
+
budget=None,
|
|
288
|
+
project_root=None,
|
|
238
289
|
) -> Result:
|
|
239
290
|
try:
|
|
240
291
|
op_str = str(op or "")
|
|
@@ -258,20 +309,28 @@ class Gateway:
|
|
|
258
309
|
hint="this token's role is not scoped to that "
|
|
259
310
|
"project_root (see the [roots] table in auth.toml)")
|
|
260
311
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
312
|
+
if not self._oneshot:
|
|
313
|
+
try:
|
|
314
|
+
self._reindexer.maybe_reindex(str(project_root or ""))
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
265
317
|
|
|
266
318
|
# If a reindex is running, this answer comes from the PREVIOUS index. Structural
|
|
267
319
|
# answers (callers/impact/hotspots) hash a symbol name, not file bytes, so nothing
|
|
268
320
|
# else in the envelope can reveal that — and an agent that just edited and asked
|
|
269
321
|
# "what did I break?" lands precisely here. Busting the cache would not help: the
|
|
270
322
|
# index itself is behind, so re-asking refetches the same stale data.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
323
|
+
# In one-shot mode this is always False and the flag is simply never emitted: no
|
|
324
|
+
# reindex was started, so there is nothing for the answer to be behind. In server mode
|
|
325
|
+
# the flag now means what it always claimed to — a pass this process did not start is
|
|
326
|
+
# genuinely still running — which is why it is fixed at the cause rather than deleted.
|
|
327
|
+
if self._oneshot:
|
|
274
328
|
reindexing = False
|
|
329
|
+
else:
|
|
330
|
+
try:
|
|
331
|
+
reindexing = self._reindexer.reindex_pending(str(project_root or ""))
|
|
332
|
+
except Exception:
|
|
333
|
+
reindexing = False
|
|
275
334
|
|
|
276
335
|
# Legacy list-based path (backward compat with pre-Phase-2 tests)
|
|
277
336
|
if self._legacy_providers is not None:
|
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|
|
11
11
|
|
|
12
12
|
from codeintel.containment import contained_path, real_root
|
|
13
13
|
from codeintel.source_kind import (
|
|
14
|
+
CODE_EXTS,
|
|
14
15
|
load_gitattributes_globs,
|
|
15
16
|
looks_generated_file,
|
|
16
17
|
looks_generated_path,
|
|
@@ -31,12 +32,10 @@ _MAX_CHUNK_CHARS = 200_000
|
|
|
31
32
|
# signal and is what `git` itself uses.
|
|
32
33
|
_BINARY_SNIFF_BYTES = 8192
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".hh", # C/C++ variants
|
|
39
|
-
})
|
|
35
|
+
# Derived from the shared policy rather than repeated: the corpus is "code, plus markdown". Two
|
|
36
|
+
# hand-typed copies of one population is how the sign-out bug in the review notes happened — one
|
|
37
|
+
# copy drifts and nothing says so.
|
|
38
|
+
_INDEXED_EXTS = CODE_EXTS | {".md"}
|
|
40
39
|
_SKIP_DIRS = frozenset({"__pycache__", ".git", "node_modules"})
|
|
41
40
|
# Directory names that mean "generated" only at the REPO ROOT. Matching them at any depth hid real
|
|
42
41
|
# source: `coverage/` is the coverage.py package, `src/build/` is pypa/build's entire codebase
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Line-number conversion for the backends whose numbering differs from ours.
|
|
2
|
+
|
|
3
|
+
**This module does not apply to every backend, and the line base is data, not a rule.** An earlier
|
|
4
|
+
version of this docstring asserted "the only place in codeintel permitted to format a path:line" and
|
|
5
|
+
"every backend we speak to counts lines from zero". The second claim is false, and an investigation
|
|
6
|
+
found that following the first would have introduced a NEW off-by-one: the graph backend
|
|
7
|
+
(codebase-memory-mcp) reports 1-based line numbers, so ``graph.py`` formats its own ``path:line``
|
|
8
|
+
directly and must keep doing so. Centralising a guarantee only removes a defect class when the
|
|
9
|
+
implementations agree on the semantics being centralised — here they do not.
|
|
10
|
+
|
|
11
|
+
LINE BASES, verified by probing each backend — see LINE_BASES below:
|
|
12
|
+
serena / LSP -> use loc()/span()
|
|
13
|
+
semantic chunk_start -> use loc() (chunk_start = max(0, start - 1), indexer.py)
|
|
14
|
+
codebase-memory-mcp -> see LINE_BASES below; do NOT use loc(); emit as-is
|
|
15
|
+
|
|
16
|
+
Use ``loc()``/``span()`` for the 0-based backends. Every human, editor and terminal that consumes
|
|
17
|
+
our output counts from one, and agent hosts turn ``path:line`` into a clickable link. Each
|
|
18
|
+
renderer used to do its own conversion, which meant each renderer got to forget it independently
|
|
19
|
+
— and two of them did:
|
|
20
|
+
|
|
21
|
+
- ``lsp.py`` emitted serena's ``body_location`` and reference markers verbatim;
|
|
22
|
+
- ``semantic.py`` emitted ``chunk_start``, which is 0-based by construction in ``indexer.py``.
|
|
23
|
+
|
|
24
|
+
Both shipped answers pointing one line above the truth, and both produced ``path:0`` for anything
|
|
25
|
+
at the top of a file — a line number that does not exist, which is the tell that survived into the
|
|
26
|
+
evaluation transcript before anyone noticed it.
|
|
27
|
+
|
|
28
|
+
Centralising the conversion is what stops the next renderer from re-deciding this. If you need a
|
|
29
|
+
``path:line`` string, call ``loc()``; if you need the number alone, call ``line1()``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
# The measured line base of each backend. The prose table above is the explanation; THIS is the
|
|
35
|
+
# enforced form (tests/test_loc_census.py drives every provider and checks the number it emits
|
|
36
|
+
# against this map). A new provider file with no entry here fails the census.
|
|
37
|
+
LINE_BASES: dict[str, int] = {"lsp": 0, "semantic": 0, "graph": 1}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def line1(line0: object) -> int | None:
|
|
41
|
+
"""Convert a backend's 0-based line number to a 1-based one.
|
|
42
|
+
|
|
43
|
+
Returns None for anything that is not a usable line number, so callers render a bare path
|
|
44
|
+
rather than inventing a position. Accepts str because some backends report line numbers as
|
|
45
|
+
text inside a larger payload.
|
|
46
|
+
"""
|
|
47
|
+
if line0 is None or isinstance(line0, bool):
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
n = int(line0) # type: ignore[call-overload]
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
return None
|
|
53
|
+
# A negative line is meaningless; clamp rather than emit `path:-1` or `path:0`.
|
|
54
|
+
return n + 1 if n >= 0 else None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def loc(path: object, line0: object = None) -> str:
|
|
58
|
+
"""Render ``path:line`` with the line converted to 1-based, or a bare path when there is no
|
|
59
|
+
usable line. ``line0`` is the backend's own 0-based number — never pre-increment it."""
|
|
60
|
+
p = str(path or "").strip() or "?"
|
|
61
|
+
n = line1(line0)
|
|
62
|
+
return f"{p}:{n}" if n is not None else p
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def span(path: object, start0: object, end0: object = None) -> str:
|
|
66
|
+
"""Render ``path:start-end`` (1-based) for a symbol body, or ``path:start``/``path`` when the
|
|
67
|
+
end is absent or unusable."""
|
|
68
|
+
p = str(path or "").strip() or "?"
|
|
69
|
+
s = line1(start0)
|
|
70
|
+
if s is None:
|
|
71
|
+
return p
|
|
72
|
+
e = line1(end0)
|
|
73
|
+
return f"{p}:{s}-{e}" if e is not None else f"{p}:{s}"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""A typed result for internal seams, so a failure stays a failure all the way up.
|
|
2
|
+
|
|
3
|
+
Every provider-internal helper used to return ``X | None``, and ``None`` there meant five
|
|
4
|
+
different things: never asked · timed out · the backend errored · the payload did not parse ·
|
|
5
|
+
genuinely empty. Callers could not tell them apart, so they picked the cheapest reading — and the
|
|
6
|
+
cheapest reading of "no references came back" is "this symbol has no references", which is the
|
|
7
|
+
permissive answer and the wrong one.
|
|
8
|
+
|
|
9
|
+
That collapse is what produced the worst bug in the 2026-08-17 evaluation: a cold language server
|
|
10
|
+
timed out, ``_call_tool`` returned ``None``, and four conversions later the caller rendered
|
|
11
|
+
``## References\\n(none)`` — a confident false statement, with no ``reason`` and no ``hint``,
|
|
12
|
+
byte-identical to a true one.
|
|
13
|
+
|
|
14
|
+
``graph.py`` had already worked this out and re-invented a fix for it three separate times in one
|
|
15
|
+
module (``_FAIL``/``_UNPARSABLE``, ``ProjectLookup``, and ``_search_symbols`` returning ``None``
|
|
16
|
+
vs ``[]`` on purpose). ``lsp.py`` had none of them, which is exactly where the critical bug lived.
|
|
17
|
+
This is that abstraction, written once.
|
|
18
|
+
|
|
19
|
+
Rule: a helper returns ``Missing`` when it could not answer, and ``Ok`` when it did — including
|
|
20
|
+
``Ok`` of an empty collection, which is a real and useful answer meaning "asked, and there is
|
|
21
|
+
nothing". Never conflate the two.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Generic, Literal, TypeAlias, TypeVar
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
|
|
31
|
+
# Why a call produced no value. These strings reach the caller inside a gap note, so they are
|
|
32
|
+
# written to be read by a person or an agent, not only matched by code.
|
|
33
|
+
MissingKind = Literal[
|
|
34
|
+
"not-asked", # a precondition was absent, so the call was never made
|
|
35
|
+
"timeout", # the backend did not answer inside the budget
|
|
36
|
+
"backend-error", # the backend answered, and the answer was an error
|
|
37
|
+
"unparsable", # the backend answered, and the payload could not be read
|
|
38
|
+
"unsupported", # this backend cannot answer this question at all
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
_DETAIL: dict[str, str] = {
|
|
42
|
+
"not-asked": "a precondition for this lookup was missing, so it was never requested",
|
|
43
|
+
"timeout": "the backend did not respond within the time budget",
|
|
44
|
+
"backend-error": "the backend returned an error instead of an answer",
|
|
45
|
+
"unparsable": "the backend's response could not be parsed",
|
|
46
|
+
"unsupported": "this engine cannot answer that",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Ok(Generic[T]):
|
|
52
|
+
"""The call answered. ``value`` may legitimately be empty."""
|
|
53
|
+
|
|
54
|
+
value: T
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class Missing:
|
|
59
|
+
"""The call did not answer, and this is why. Never rendered as a value."""
|
|
60
|
+
|
|
61
|
+
kind: MissingKind
|
|
62
|
+
detail: str = ""
|
|
63
|
+
retry_after_s: float | None = None
|
|
64
|
+
|
|
65
|
+
def describe(self) -> str:
|
|
66
|
+
"""One caller-facing sentence. Falls back to a default phrasing per kind so that every
|
|
67
|
+
Missing carries something readable even when the call site passed no detail."""
|
|
68
|
+
return self.detail or _DETAIL.get(self.kind, "this lookup did not complete")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
Outcome: TypeAlias = "Ok[T] | Missing"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_missing(outcome: object) -> bool:
|
|
75
|
+
"""True when an outcome represents a failure to answer. Kept as a helper so call sites read
|
|
76
|
+
as intent rather than as an isinstance check against an implementation detail."""
|
|
77
|
+
return isinstance(outcome, Missing)
|
|
@@ -37,6 +37,17 @@ class Result(TypedDict):
|
|
|
37
37
|
# reflects the last COMPLETED index rather than the current source. Optional, and the MCP
|
|
38
38
|
# tools deliberately return plain `dict` so this never becomes a required schema field.
|
|
39
39
|
reindexing: NotRequired[bool]
|
|
40
|
+
# How much of the answer the engine vouches for. A non-null `result` used to be an implicit
|
|
41
|
+
# promise of completeness, which is how a timed-out reference lookup shipped as "(none)".
|
|
42
|
+
# "complete" — ran; the engine stands behind the whole body
|
|
43
|
+
# "partial" — ran; produced a body; a NAMED part of it is known to be missing (see `gaps`)
|
|
44
|
+
# Absent on null results, where `reason` already carries the whole story.
|
|
45
|
+
confidence: NotRequired[str]
|
|
46
|
+
# One entry per part of the answer that could not be retrieved:
|
|
47
|
+
# {"section": str, "kind": str, "detail": str, "retry_after_s": float?}. Emitted only when
|
|
48
|
+
# non-empty. The same fact is always rendered into `result` too, because that is the field an
|
|
49
|
+
# agent actually reads.
|
|
50
|
+
gaps: NotRequired[list[dict[str, Any]]]
|
|
40
51
|
|
|
41
52
|
|
|
42
53
|
@runtime_checkable
|
|
@@ -74,3 +85,32 @@ def safe_null_result(
|
|
|
74
85
|
if hint is not None:
|
|
75
86
|
r["hint"] = hint
|
|
76
87
|
return r
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def attach_confidence(result: Result, gaps: Any = ()) -> Result:
|
|
91
|
+
"""Stamp an answered envelope with how much of it the engine vouches for.
|
|
92
|
+
|
|
93
|
+
Every engine must call this, and that is the point. `confidence` was introduced on the LSP
|
|
94
|
+
provider alone, which reproduced in miniature the defect it was added to fix: the instructions
|
|
95
|
+
told callers to check a field that two of three engines never set, and an ABSENT field is
|
|
96
|
+
ambiguous — "complete" and "this engine does not report" look identical. A contract that only
|
|
97
|
+
one implementation honours is not a contract.
|
|
98
|
+
|
|
99
|
+
`gaps` is a sequence of {"section", "kind", "detail", "retry_after_s"?} dicts. Empty ⇒
|
|
100
|
+
`complete`; non-empty ⇒ `partial`, and the caller is expected to have said the same thing in
|
|
101
|
+
the body text, because that is the field an agent actually reads.
|
|
102
|
+
|
|
103
|
+
Null results are left alone: `reason` already carries the whole story there, and stamping them
|
|
104
|
+
would imply a body exists to be partial about.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
if result.get("result") is None:
|
|
108
|
+
return result
|
|
109
|
+
items = [g for g in (gaps or ()) if isinstance(g, dict)]
|
|
110
|
+
out: Result = {**result, "confidence": "partial" if items else "complete"}
|
|
111
|
+
if items:
|
|
112
|
+
out["gaps"] = items
|
|
113
|
+
return out
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
log_swallowed("attach_confidence", exc)
|
|
116
|
+
return result
|