codecortex 0.23.1__tar.gz → 0.23.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.23.1/src/codecortex.egg-info → codecortex-0.23.2}/PKG-INFO +1 -1
- {codecortex-0.23.1 → codecortex-0.23.2/src/codecortex.egg-info}/PKG-INFO +1 -1
- codecortex-0.23.2/src/codeintel/__init__.py +1 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/gateway.py +16 -11
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/indexer.py +13 -1
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/provider.py +17 -6
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/providers/lsp.py +44 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_doctor.py +21 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_gateway.py +1 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_incompleteness.py +30 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_index_failure_reason.py +25 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_lsp_provider.py +28 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_semantic_provider.py +1 -0
- codecortex-0.23.1/src/codeintel/__init__.py +0 -1
- {codecortex-0.23.1 → codecortex-0.23.2}/LICENSE +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/README.md +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/pyproject.toml +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/setup.cfg +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codecortex.egg-info/SOURCES.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/__main__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/agent_prompt.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/auth.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/c4.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/c4_check.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/c4_layers.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/cache.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/__init__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/_common.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/c4.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/doctor.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/gen_token.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/graph.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/index.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/install.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/map.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/prompt.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/query.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/reset.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/serve.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/serve_http.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/setup.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/commands/status.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/containment.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/doctor.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/graph_backend.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/graph_render.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/graph_resolution.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/grapher.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/http_server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/injector.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/installer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/lang_config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/loc.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/mapper.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/metrics.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/outcome.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/paths.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/policy.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/progress.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/py.typed +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/query_ops.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/redact.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/reset.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/searcher.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/source_kind.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/term.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/verify.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/src/codeintel/wire_text.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_agent_prompt.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_backend_reaper.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_background_index_failure.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_bench_oracle.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_bench_oracle_ts.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_c4.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_c4_check.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_c4_layers.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_c4_model.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_c4_output.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_cache.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_chunking.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_cli_commands.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_cli_help.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_cli_install_dry_run.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_cli_install_offer_injection.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_cold_process.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_corpus.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_could_not_ask_semantic.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_docs_ci_claims.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_docs_deadcode_withdrawal.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_e2e.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_edge_confidence.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_enterprise.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_graph_failure_population.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_graph_provider.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_graph_real.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_grapher.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_hard_exit.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_hardening.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_http_auth.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_http_server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_index_progress.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_indexer_binary.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_installer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_integration.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_lang_config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_loc_census.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_lsp_language_coverage.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_lsp_real.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_mapper.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_mcp_server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_model_dimension.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_never_raise.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_null_reason_and_ripple.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_onboarding.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_rbac.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_redaction_boundary.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_reindexer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_rerank.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_reset.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_source_kind.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_staleness.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_symbol_context.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_term.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_treesitter.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_verify.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_verify_call.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_version_skew.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.2}/tests/test_wire_text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.23.2"
|
|
@@ -292,17 +292,22 @@ class Gateway:
|
|
|
292
292
|
# codebase throws away the could-not-ask / asked-and-found-nothing distinction it is
|
|
293
293
|
# otherwise careful to preserve per-provider.
|
|
294
294
|
reasons = {eng: str(r.get("reason") or "no-result") for eng, r in results.items()}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
295
|
+
# Providers have already classified their reason into the public outcome taxonomy.
|
|
296
|
+
# Repeating a private reason allow-list here guaranteed drift: the first new reason,
|
|
297
|
+
# ``source-unreadable``, was correctly marked unavailable by LSP and then collapsed
|
|
298
|
+
# back to ``not_found`` by this merge. Failed and unavailable both mean no engine was
|
|
299
|
+
# able to answer; an actual miss is the explicit ``not_found`` outcome.
|
|
300
|
+
# Older/custom providers may not have adopted ``outcome`` yet. Route their reason
|
|
301
|
+
# through the SAME central classifier rather than reintroducing a local allow-list.
|
|
302
|
+
classified = [
|
|
303
|
+
r.get("outcome") or safe_null_result(
|
|
304
|
+
op_str, target_str, engine=eng, reason=reasons[eng]
|
|
305
|
+
).get("outcome")
|
|
306
|
+
for eng, r in results.items()
|
|
307
|
+
]
|
|
308
|
+
all_unreachable = bool(classified) and all(
|
|
309
|
+
outcome in ("unavailable", "failed") for outcome in classified
|
|
310
|
+
)
|
|
306
311
|
summary = "engines-unavailable" if all_unreachable else "no-result"
|
|
307
312
|
detail = ", ".join(f"{eng}: {why}" for eng, why in sorted(reasons.items()))
|
|
308
313
|
return safe_null_result(
|
|
@@ -390,7 +390,19 @@ class Indexer:
|
|
|
390
390
|
root_real = real_root(str(root))
|
|
391
391
|
except Exception:
|
|
392
392
|
root_real = str(root)
|
|
393
|
-
|
|
393
|
+
def fail_on_unreadable_tree(error: OSError) -> None:
|
|
394
|
+
"""Do not turn an unreadable subtree into a successful empty index.
|
|
395
|
+
|
|
396
|
+
``os.walk`` silently ignores directory-enumeration failures unless an ``onerror``
|
|
397
|
+
callback is supplied. On macOS this is particularly dangerous for folders protected
|
|
398
|
+
by Files & Folders / Full Disk Access: the root can pass ``isdir`` while every source
|
|
399
|
+
directory raises ``EPERM``, leaving the CLI to report "Nothing new to index" at exit
|
|
400
|
+
zero. Propagating the original error preserves its path and lets ``index()`` expose a
|
|
401
|
+
truthful, actionable failure.
|
|
402
|
+
"""
|
|
403
|
+
raise error
|
|
404
|
+
|
|
405
|
+
for dirpath, dirnames, filenames in os.walk(root, onerror=fail_on_unreadable_tree):
|
|
394
406
|
at_root = os.path.realpath(dirpath) == os.path.realpath(real_root_str)
|
|
395
407
|
dirnames[:] = [
|
|
396
408
|
d for d in dirnames
|
|
@@ -75,20 +75,31 @@ def safe_null_result(
|
|
|
75
75
|
hint: str | None = None,
|
|
76
76
|
) -> Result:
|
|
77
77
|
failed_reasons = {
|
|
78
|
-
"backend-error", "backend-incompatible", "boot-failed", "error", "
|
|
79
|
-
"provider-error", "
|
|
78
|
+
"backend-error", "backend-incompatible", "boot-failed", "error", "gateway-error",
|
|
79
|
+
"handler-error", "index-failed", "provider-error", "query-failed", "timeout",
|
|
80
|
+
"unparsable",
|
|
80
81
|
}
|
|
81
82
|
unavailable_reasons = {
|
|
82
|
-
"engine-unavailable", "engines-unavailable", "indexing-in-progress",
|
|
83
|
-
"no-
|
|
84
|
-
"
|
|
83
|
+
"backend-unreachable", "engine-unavailable", "engines-unavailable", "indexing-in-progress",
|
|
84
|
+
"index-stale", "no-engine", "no-project-root", "op-not-allowed-for-role",
|
|
85
|
+
"op-not-supported", "op-withdrawn", "project-not-indexed",
|
|
86
|
+
"project-not-indexed-standalone", "root-not-allowed-for-role", "source-unreadable",
|
|
87
|
+
"unknown-engine", "unsupported-op", "warming",
|
|
88
|
+
}
|
|
89
|
+
not_found_reasons = {
|
|
90
|
+
"below-floor", "no-edges", "no-index", "no-result", "not-found", "not-in-graph",
|
|
85
91
|
}
|
|
86
92
|
if reason in failed_reasons:
|
|
87
93
|
outcome = "failed"
|
|
88
94
|
elif reason in unavailable_reasons:
|
|
89
95
|
outcome = "unavailable"
|
|
90
|
-
|
|
96
|
+
elif reason in not_found_reasons:
|
|
91
97
|
outcome = "not_found"
|
|
98
|
+
else:
|
|
99
|
+
# New failure reasons must fail closed. Defaulting every unknown string to ``not_found``
|
|
100
|
+
# turned a newly introduced ``source-unreadable`` reason into an assertion that the symbol
|
|
101
|
+
# was absent — exactly the ambiguity the explicit outcome field was added to remove.
|
|
102
|
+
outcome = "failed"
|
|
92
103
|
r: Result = {
|
|
93
104
|
"ok": True,
|
|
94
105
|
"op": str(op or ""),
|
|
@@ -127,6 +127,29 @@ def _serena_launch_args(cmd: str, project_root: str) -> list[str]:
|
|
|
127
127
|
return [cmd, *common]
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
def _project_access_failure(project_root: str) -> tuple[str, str] | None:
|
|
131
|
+
"""Return a safe diagnosis when the host cannot enumerate the project root.
|
|
132
|
+
|
|
133
|
+
Serena's stdio transport wraps an early subprocess exit in an ``ExceptionGroup``. When the
|
|
134
|
+
real cause is a macOS Files & Folders denial, reporting that wrapper as a network/bootstrap
|
|
135
|
+
failure sends the user in exactly the wrong direction. A one-entry scan is cheap, local, and
|
|
136
|
+
establishes the prerequisite Serena itself needs without forwarding any backend-controlled
|
|
137
|
+
prose to the calling agent.
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
with os.scandir(project_root) as entries:
|
|
141
|
+
next(entries, None)
|
|
142
|
+
except OSError as exc:
|
|
143
|
+
location = str(getattr(exc, "filename", None) or project_root)
|
|
144
|
+
detail = f"repository root is not readable by codeintel ({type(exc).__name__}: {location})"
|
|
145
|
+
remediation = (
|
|
146
|
+
"grant the codeintel host read access to the repository, then retry; on macOS check "
|
|
147
|
+
"System Settings > Privacy & Security > Files and Folders (or Full Disk Access)"
|
|
148
|
+
)
|
|
149
|
+
return detail, remediation
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
130
153
|
class _State(enum.Enum):
|
|
131
154
|
WARMING = "WARMING"
|
|
132
155
|
READY = "READY"
|
|
@@ -433,6 +456,12 @@ class LspProvider:
|
|
|
433
456
|
"detail": "serena session is warming for this repo", "remediation": None}
|
|
434
457
|
|
|
435
458
|
# deep: boot (or reuse) a session and poll to a hard deadline — never hangs.
|
|
459
|
+
access_failure = _project_access_failure(project_root)
|
|
460
|
+
if access_failure is not None:
|
|
461
|
+
detail, remediation = access_failure
|
|
462
|
+
return {"installed": True, "runnable": False, "repo_indexed": None,
|
|
463
|
+
"detail": detail, "remediation": remediation}
|
|
464
|
+
|
|
436
465
|
session = self._get_or_create_session(project_root)
|
|
437
466
|
deadline = time.monotonic() + timeout_s
|
|
438
467
|
while time.monotonic() < deadline:
|
|
@@ -502,6 +531,21 @@ class LspProvider:
|
|
|
502
531
|
if not self.available:
|
|
503
532
|
return safe_null_result(op_str, target_str, engine="lsp", reason="engine-unavailable")
|
|
504
533
|
|
|
534
|
+
# A protected macOS folder can still satisfy ``isdir`` while denying enumeration.
|
|
535
|
+
# Detect that locally before spawning Serena: otherwise its early exit is wrapped by
|
|
536
|
+
# the MCP transport as a generic boot failure and the first-query hint talks about a
|
|
537
|
+
# cold uvx cache instead of the permission the user actually needs to grant. Keep the
|
|
538
|
+
# ``isdir`` guard so synthetic/nonexistent roots used by embedders retain the existing
|
|
539
|
+
# never-raise backend behaviour.
|
|
540
|
+
if os.path.isdir(root_str):
|
|
541
|
+
access_failure = _project_access_failure(root_str)
|
|
542
|
+
if access_failure is not None:
|
|
543
|
+
detail, remediation = access_failure
|
|
544
|
+
return safe_null_result(
|
|
545
|
+
op_str, target_str, engine="lsp", reason="source-unreadable",
|
|
546
|
+
hint=f"{detail}; {remediation}",
|
|
547
|
+
)
|
|
548
|
+
|
|
505
549
|
try:
|
|
506
550
|
budget_ms = int(budget) if budget else 0
|
|
507
551
|
except Exception:
|
|
@@ -316,6 +316,27 @@ def test_lsp_probe_deep_boot_failure_is_bounded(monkeypatch):
|
|
|
316
316
|
assert r["runnable"] in (False, None) # failed to boot (or timed out) — never True
|
|
317
317
|
|
|
318
318
|
|
|
319
|
+
def test_lsp_probe_reports_repository_access_before_blaming_network(monkeypatch):
|
|
320
|
+
monkeypatch.setattr("codeintel.providers.lsp.shutil.which", lambda x: "/fake/uvx")
|
|
321
|
+
monkeypatch.setattr(
|
|
322
|
+
"codeintel.providers.lsp.os.scandir",
|
|
323
|
+
lambda root: (_ for _ in ()).throw(PermissionError(1, "denied", str(root))),
|
|
324
|
+
)
|
|
325
|
+
p = LspProvider()
|
|
326
|
+
monkeypatch.setattr(
|
|
327
|
+
p, "_get_or_create_session",
|
|
328
|
+
lambda root: pytest.fail("an unreadable repository must not launch serena"),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
r = p.probe("/protected/repo", deep=True)
|
|
332
|
+
|
|
333
|
+
assert r["runnable"] is False
|
|
334
|
+
assert "PermissionError" in r["detail"]
|
|
335
|
+
assert "/protected/repo" in r["detail"]
|
|
336
|
+
assert "Privacy & Security" in r["remediation"]
|
|
337
|
+
assert "network" not in r["remediation"].lower()
|
|
338
|
+
|
|
339
|
+
|
|
319
340
|
# --------------------------------------------------------------------------- #
|
|
320
341
|
# handler + hint plumbing
|
|
321
342
|
# --------------------------------------------------------------------------- #
|
|
@@ -548,6 +548,7 @@ def test_a_fanout_where_engines_ran_and_found_nothing_still_reports_no_result():
|
|
|
548
548
|
lsp=_NullProvider("lsp", "not-found"))
|
|
549
549
|
r = gw.query(op="context", target="nope", engine="both")
|
|
550
550
|
assert r["reason"] == "no-result"
|
|
551
|
+
assert r["outcome"] == "not_found"
|
|
551
552
|
assert "NOT evidence" not in (r.get("hint") or "")
|
|
552
553
|
|
|
553
554
|
|
|
@@ -371,8 +371,13 @@ def test_null_result_outcome_is_machine_readable():
|
|
|
371
371
|
from codeintel.provider import safe_null_result
|
|
372
372
|
|
|
373
373
|
assert safe_null_result("x", "y", reason="not-in-graph")["outcome"] == "not_found"
|
|
374
|
+
assert safe_null_result("x", "y", reason="not-found")["outcome"] == "not_found"
|
|
375
|
+
assert safe_null_result("x", "y", reason="no-index")["outcome"] == "not_found"
|
|
374
376
|
assert safe_null_result("x", "y", reason="engine-unavailable")["outcome"] == "unavailable"
|
|
375
377
|
assert safe_null_result("x", "y", reason="backend-error")["outcome"] == "failed"
|
|
378
|
+
assert safe_null_result("x", "y", reason="source-unreadable")["outcome"] == "unavailable"
|
|
379
|
+
assert safe_null_result("x", "y", reason="backend-unreachable")["outcome"] == "unavailable"
|
|
380
|
+
assert safe_null_result("x", "y", reason="new-unclassified-reason")["outcome"] == "failed"
|
|
376
381
|
|
|
377
382
|
|
|
378
383
|
def test_a_fanned_out_answer_reports_the_engine_that_could_not_be_asked():
|
|
@@ -398,6 +403,31 @@ def test_a_fanned_out_answer_reports_the_engine_that_could_not_be_asked():
|
|
|
398
403
|
assert "engine-unavailable" in kinds, kinds
|
|
399
404
|
|
|
400
405
|
|
|
406
|
+
def test_fan_out_preserves_source_unreadable_as_unavailable():
|
|
407
|
+
"""The merge must consume the public outcome, not maintain another reason allow-list. If all
|
|
408
|
+
engines could not answer, a newly introduced reason must never collapse back to ``not_found``.
|
|
409
|
+
"""
|
|
410
|
+
from codeintel.gateway import Gateway
|
|
411
|
+
from codeintel.provider import safe_null_result
|
|
412
|
+
|
|
413
|
+
merged = Gateway()._merge(
|
|
414
|
+
{
|
|
415
|
+
"lsp": safe_null_result(
|
|
416
|
+
"context", "createSession", engine="lsp", reason="source-unreadable"
|
|
417
|
+
),
|
|
418
|
+
"graph": safe_null_result(
|
|
419
|
+
"context", "createSession", engine="graph", reason="project-not-indexed"
|
|
420
|
+
),
|
|
421
|
+
},
|
|
422
|
+
"context", "createSession",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
assert merged["result"] is None
|
|
426
|
+
assert merged["reason"] == "engines-unavailable"
|
|
427
|
+
assert merged["outcome"] == "unavailable"
|
|
428
|
+
assert "NOT evidence" in merged["hint"]
|
|
429
|
+
|
|
430
|
+
|
|
401
431
|
def test_a_constituent_gap_survives_the_merge():
|
|
402
432
|
from codeintel.gateway import Gateway
|
|
403
433
|
|
|
@@ -326,6 +326,31 @@ def test_other_failures_keep_their_type_prefix(monkeypatch, tmp_path):
|
|
|
326
326
|
assert indexer.last_error == "PermissionError: read-only file system"
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
def test_an_unreadable_subtree_is_not_a_successful_empty_walk(monkeypatch, tmp_path):
|
|
330
|
+
"""``os.walk`` ignores enumeration errors by default. A macOS-protected repository could
|
|
331
|
+
therefore scan as zero files and make the CLI print "Nothing new to index" at exit zero even
|
|
332
|
+
though no source was readable. The original permission error, including its path, must escape
|
|
333
|
+
the walk so ``Indexer.index`` can report it as an unrecoverable failure.
|
|
334
|
+
"""
|
|
335
|
+
import codeintel.indexer as indexer_module
|
|
336
|
+
from codeintel.indexer import Indexer
|
|
337
|
+
|
|
338
|
+
protected = tmp_path / "protected-repo"
|
|
339
|
+
|
|
340
|
+
def denied_walk(root, *, onerror=None):
|
|
341
|
+
assert onerror is not None
|
|
342
|
+
onerror(PermissionError(1, "Operation not permitted", str(protected)))
|
|
343
|
+
return []
|
|
344
|
+
|
|
345
|
+
monkeypatch.setattr(indexer_module.os, "walk", denied_walk)
|
|
346
|
+
monkeypatch.setattr(indexer_module, "load_gitattributes_globs", lambda root: [])
|
|
347
|
+
indexer = Indexer.__new__(Indexer)
|
|
348
|
+
monkeypatch.setattr(indexer, "_load_gitignore", lambda root: set())
|
|
349
|
+
|
|
350
|
+
with pytest.raises(PermissionError, match="protected-repo"):
|
|
351
|
+
list(indexer._walk_files(tmp_path))
|
|
352
|
+
|
|
353
|
+
|
|
329
354
|
def test_the_searcher_reports_it_too(monkeypatch):
|
|
330
355
|
"""A query is the other way a cold cache is discovered — stage-qualified, message verbatim."""
|
|
331
356
|
from codeintel.searcher import Searcher
|
|
@@ -6,6 +6,8 @@ import time
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
from unittest.mock import MagicMock
|
|
8
8
|
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
9
11
|
from codeintel.outcome import Missing, Ok
|
|
10
12
|
from codeintel.providers.lsp import LspProvider, _State
|
|
11
13
|
from codeintel.server import code_status_handler
|
|
@@ -462,6 +464,32 @@ def test_boot_failed_on_a_cold_uvx_first_attempt_reads_as_retry(monkeypatch):
|
|
|
462
464
|
assert "download" in hint
|
|
463
465
|
|
|
464
466
|
|
|
467
|
+
def test_an_unreadable_repository_never_launches_serena(monkeypatch):
|
|
468
|
+
monkeypatch.setattr("codeintel.providers.lsp.shutil.which",
|
|
469
|
+
lambda x: "/fake/uvx" if x == "uvx" else None)
|
|
470
|
+
monkeypatch.setattr("codeintel.providers.lsp.os.path.isdir", lambda root: True)
|
|
471
|
+
monkeypatch.setattr(
|
|
472
|
+
"codeintel.providers.lsp._project_access_failure",
|
|
473
|
+
lambda root: (
|
|
474
|
+
f"repository root is not readable by codeintel (PermissionError: {root})",
|
|
475
|
+
"grant repository access",
|
|
476
|
+
),
|
|
477
|
+
)
|
|
478
|
+
p = LspProvider()
|
|
479
|
+
monkeypatch.setattr(
|
|
480
|
+
p, "_get_or_create_session",
|
|
481
|
+
lambda root: pytest.fail("an unreadable repository must not launch serena"),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
r = p.build_result("symbol", "createSession", [], 1000, "/protected/repo")
|
|
485
|
+
|
|
486
|
+
assert r["result"] is None
|
|
487
|
+
assert r["reason"] == "source-unreadable"
|
|
488
|
+
assert r["outcome"] == "unavailable"
|
|
489
|
+
assert "PermissionError" in r["hint"]
|
|
490
|
+
assert "grant repository access" in r["hint"]
|
|
491
|
+
|
|
492
|
+
|
|
465
493
|
def test_boot_failed_on_a_respawn_does_not_blame_a_cold_cache(monkeypatch):
|
|
466
494
|
"""`uvx` populated its cache on the first attempt, so "still downloading" is only true once."""
|
|
467
495
|
monkeypatch.setattr("codeintel.providers.lsp.shutil.which",
|
|
@@ -478,4 +478,5 @@ def test_a_fanout_where_the_repo_is_merely_unindexed_keeps_its_ordinary_summary(
|
|
|
478
478
|
r = gw.query(op="context", target="make_widget", engine="all", project_root="/x")
|
|
479
479
|
|
|
480
480
|
assert r["reason"] == "no-result"
|
|
481
|
+
assert r["outcome"] == "not_found"
|
|
481
482
|
assert "NOT evidence" not in (r.get("hint") or "")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.23.1"
|
|
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
|
|
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
|