codecortex 0.23.2__tar.gz → 0.23.3__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.
Files changed (148) hide show
  1. {codecortex-0.23.2/src/codecortex.egg-info → codecortex-0.23.3}/PKG-INFO +1 -1
  2. {codecortex-0.23.2 → codecortex-0.23.3/src/codecortex.egg-info}/PKG-INFO +1 -1
  3. codecortex-0.23.3/src/codeintel/__init__.py +1 -0
  4. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/gateway.py +46 -23
  5. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/providers/lsp.py +48 -1
  6. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_lsp_provider.py +58 -0
  7. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_null_reason_and_ripple.py +64 -0
  8. codecortex-0.23.2/src/codeintel/__init__.py +0 -1
  9. {codecortex-0.23.2 → codecortex-0.23.3}/LICENSE +0 -0
  10. {codecortex-0.23.2 → codecortex-0.23.3}/README.md +0 -0
  11. {codecortex-0.23.2 → codecortex-0.23.3}/pyproject.toml +0 -0
  12. {codecortex-0.23.2 → codecortex-0.23.3}/setup.cfg +0 -0
  13. {codecortex-0.23.2 → codecortex-0.23.3}/src/codecortex.egg-info/SOURCES.txt +0 -0
  14. {codecortex-0.23.2 → codecortex-0.23.3}/src/codecortex.egg-info/dependency_links.txt +0 -0
  15. {codecortex-0.23.2 → codecortex-0.23.3}/src/codecortex.egg-info/entry_points.txt +0 -0
  16. {codecortex-0.23.2 → codecortex-0.23.3}/src/codecortex.egg-info/requires.txt +0 -0
  17. {codecortex-0.23.2 → codecortex-0.23.3}/src/codecortex.egg-info/top_level.txt +0 -0
  18. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/__main__.py +0 -0
  19. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/agent_prompt.py +0 -0
  20. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/auth.py +0 -0
  21. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/c4.py +0 -0
  22. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/c4_check.py +0 -0
  23. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/c4_layers.py +0 -0
  24. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/cache.py +0 -0
  25. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/__init__.py +0 -0
  26. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/_common.py +0 -0
  27. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/c4.py +0 -0
  28. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/doctor.py +0 -0
  29. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/gen_token.py +0 -0
  30. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/graph.py +0 -0
  31. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/index.py +0 -0
  32. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/install.py +0 -0
  33. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/map.py +0 -0
  34. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/prompt.py +0 -0
  35. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/query.py +0 -0
  36. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/reset.py +0 -0
  37. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/serve.py +0 -0
  38. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/serve_http.py +0 -0
  39. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/setup.py +0 -0
  40. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/commands/status.py +0 -0
  41. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/config.py +0 -0
  42. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/containment.py +0 -0
  43. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/doctor.py +0 -0
  44. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/graph_backend.py +0 -0
  45. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/graph_render.py +0 -0
  46. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/graph_resolution.py +0 -0
  47. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/grapher.py +0 -0
  48. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/http_server.py +0 -0
  49. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/indexer.py +0 -0
  50. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/injector.py +0 -0
  51. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/installer.py +0 -0
  52. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/lang_config.py +0 -0
  53. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/loc.py +0 -0
  54. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/logconfig.py +0 -0
  55. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/mapper.py +0 -0
  56. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/metrics.py +0 -0
  57. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/onboarding.py +0 -0
  58. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/outcome.py +0 -0
  59. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/paths.py +0 -0
  60. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/policy.py +0 -0
  61. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/progress.py +0 -0
  62. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/provider.py +0 -0
  63. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/providers/__init__.py +0 -0
  64. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/providers/graph.py +0 -0
  65. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/providers/none.py +0 -0
  66. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/providers/semantic.py +0 -0
  67. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/py.typed +0 -0
  68. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/query_ops.py +0 -0
  69. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/redact.py +0 -0
  70. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/reindexer.py +0 -0
  71. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/reset.py +0 -0
  72. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/searcher.py +0 -0
  73. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/semantic_db.py +0 -0
  74. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/server.py +0 -0
  75. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/source_kind.py +0 -0
  76. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/term.py +0 -0
  77. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/verify.py +0 -0
  78. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/viewer/__init__.py +0 -0
  79. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/viewer/graph_template.html +0 -0
  80. {codecortex-0.23.2 → codecortex-0.23.3}/src/codeintel/wire_text.py +0 -0
  81. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_agent_prompt.py +0 -0
  82. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_backend_reaper.py +0 -0
  83. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_background_index_failure.py +0 -0
  84. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_bench_oracle.py +0 -0
  85. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_bench_oracle_ts.py +0 -0
  86. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_c4.py +0 -0
  87. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_c4_check.py +0 -0
  88. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_c4_layers.py +0 -0
  89. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_c4_model.py +0 -0
  90. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_c4_output.py +0 -0
  91. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_cache.py +0 -0
  92. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_chunking.py +0 -0
  93. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_cli_commands.py +0 -0
  94. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_cli_help.py +0 -0
  95. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_cli_install_dry_run.py +0 -0
  96. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_cli_install_offer_injection.py +0 -0
  97. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_cold_process.py +0 -0
  98. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_config.py +0 -0
  99. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_corpus.py +0 -0
  100. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_could_not_ask_semantic.py +0 -0
  101. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_docs_ci_claims.py +0 -0
  102. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_docs_deadcode_withdrawal.py +0 -0
  103. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_doctor.py +0 -0
  104. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_e2e.py +0 -0
  105. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_edge_confidence.py +0 -0
  106. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_engine_adoption.py +0 -0
  107. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_enterprise.py +0 -0
  108. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_gateway.py +0 -0
  109. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_graph_failure_population.py +0 -0
  110. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_graph_provider.py +0 -0
  111. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_graph_real.py +0 -0
  112. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_graph_stdin.py +0 -0
  113. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_grapher.py +0 -0
  114. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_hard_exit.py +0 -0
  115. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_hardening.py +0 -0
  116. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_http_auth.py +0 -0
  117. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_http_server.py +0 -0
  118. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_incompleteness.py +0 -0
  119. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_index_failure_reason.py +0 -0
  120. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_index_progress.py +0 -0
  121. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_indexer_binary.py +0 -0
  122. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_installer.py +0 -0
  123. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_integration.py +0 -0
  124. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_lang_config.py +0 -0
  125. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_loc_census.py +0 -0
  126. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_lsp_language_coverage.py +0 -0
  127. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_lsp_real.py +0 -0
  128. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_mapper.py +0 -0
  129. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_mcp_handshake.py +0 -0
  130. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_mcp_server.py +0 -0
  131. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_model_dimension.py +0 -0
  132. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_never_raise.py +0 -0
  133. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_onboarding.py +0 -0
  134. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_rbac.py +0 -0
  135. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_redaction_boundary.py +0 -0
  136. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_reindexer.py +0 -0
  137. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_rerank.py +0 -0
  138. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_reset.py +0 -0
  139. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_semantic_provider.py +0 -0
  140. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_source_kind.py +0 -0
  141. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_staleness.py +0 -0
  142. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_symbol_context.py +0 -0
  143. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_term.py +0 -0
  144. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_treesitter.py +0 -0
  145. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_verify.py +0 -0
  146. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_verify_call.py +0 -0
  147. {codecortex-0.23.2 → codecortex-0.23.3}/tests/test_version_skew.py +0 -0
  148. {codecortex-0.23.2 → codecortex-0.23.3}/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.2
3
+ Version: 0.23.3
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.2
3
+ Version: 0.23.3
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.3"
@@ -174,7 +174,7 @@ class Gateway:
174
174
  self, result: Result, op: str, target: str, budget: Any,
175
175
  project_root: Any, was_auto: bool,
176
176
  ) -> Result:
177
- """Ask the LSP when the graph's whole answer was resolved by name rather than by import.
177
+ """Ask the LSP when the graph's caller answer carries a known binding failure.
178
178
 
179
179
  This closes the routing gap that let the worst failure through. `_AUTO_ENGINE` is a static
180
180
  op→engine map, not a cascade: `callers` goes to the graph and, uniquely among the ops, has
@@ -185,48 +185,71 @@ class Gateway:
185
185
  being confidently wrong is a bug; no second engine ever being consulted is the design that
186
186
  let it reach the caller.
187
187
 
188
- Deliberately narrow. It fires only when the graph itself raised `all-rows-name-resolved`
189
- every row a name guess, across enough rows for the pattern to mean something which is the
190
- collision signature and not the ordinary case of one unverified row among several. It
191
- APPENDS rather than replaces: the LSP answers a related but different question (references,
192
- not call edges), so presenting its list as the graph's would substitute one over-claim for
193
- another. And it never fires for an explicitly pinned `--engine graph`, where the caller has
194
- said which engine they want."""
188
+ Deliberately narrow. It fires when every graph row was name-resolved, or when a caller used
189
+ a file hint and the graph found same-named symbols but no edge for that exact definition.
190
+ The latter catches property calls that the TypeScript graph resolver failed to bind, while
191
+ avoiding an LSP call for ordinary healthy answers. It APPENDS rather than replaces: the LSP
192
+ answers a related but different question (references, not call edges), so presenting its
193
+ list as the graph's would substitute one over-claim for another. And it never fires for an
194
+ explicitly pinned `--engine graph`, where the caller has said which engine they want."""
195
195
  try:
196
196
  if not was_auto or op not in self._CROSS_CHECKED_OPS:
197
197
  return result
198
198
  if result.get("result") is None or self.lsp is None:
199
199
  return result
200
200
  gaps = result.get("gaps") or []
201
- if not any(g.get("kind") == "all-rows-name-resolved"
202
- for g in gaps if isinstance(g, dict)):
201
+ kinds = {
202
+ str(g.get("kind") or "") for g in gaps if isinstance(g, dict)
203
+ }
204
+ all_name_resolved = "all-rows-name-resolved" in kinds
205
+ exact_target_unbound = "target-hint-unmatched" in kinds
206
+ if not (all_name_resolved or exact_target_unbound):
203
207
  return result
208
+ graph_problem = (
209
+ "every graph row was resolved by name"
210
+ if all_name_resolved
211
+ else "the graph found no caller edge for the file-qualified symbol"
212
+ )
204
213
  probe = self._dispatch_single(
205
214
  self.lsp, "symbol", target, budget, project_root, "lsp")
206
215
  body = probe.get("result")
207
- if not body:
216
+ probe_gaps = probe.get("gaps") or []
217
+ reference_gap = next((
218
+ gap for gap in probe_gaps
219
+ if isinstance(gap, dict) and gap.get("section") == "references"
220
+ ), None)
221
+ if not body or reference_gap is not None:
208
222
  # Silence from the LSP is not agreement. Say which check did not happen — and
209
223
  # separate "not yet booted" from "had nothing", because only the first is fixed by
210
224
  # asking again. A one-shot CLI process meets a cold serena on every invocation; the
211
225
  # long-lived MCP server keeps the session warm and takes this branch once at most.
212
226
  # Waiting here is deliberately NOT done: it would hold back a graph answer that is
213
227
  # already complete, to append a section that is only advisory.
214
- warming = probe.get("reason") == "warming"
215
- why = ("the language server had not finished booting"
216
- if warming else "the LSP engine reported nothing for this symbol")
228
+ unavailable_reason = (
229
+ str(reference_gap.get("kind") or "not-asked")
230
+ if reference_gap is not None else str(probe.get("reason") or "no-result")
231
+ )
232
+ warming = unavailable_reason in {"warming", "timeout"}
233
+ why = (
234
+ "the language server had not finished booting"
235
+ if unavailable_reason == "warming"
236
+ else str(reference_gap.get("detail") or "the reference lookup did not answer")
237
+ if reference_gap is not None
238
+ else "the LSP engine reported nothing for this symbol"
239
+ )
217
240
  nxt = (" Ask again once it is warm and this section will be filled in."
218
241
  if warming else
219
242
  " Check it yourself with `--engine lsp --op symbol`.")
220
243
  return {**result, "gaps": [*gaps, {
221
244
  "section": op, "kind": "cross-check-unavailable",
222
245
  "engine": "lsp",
223
- "reason": probe.get("reason") or "no-result",
224
- "detail": f"every graph row was name-resolved and the LSP could not confirm "
225
- f"them ({why}), so they remain unverified"
246
+ "reason": unavailable_reason,
247
+ "detail": f"{graph_problem} and the LSP could not provide an independent "
248
+ f"reference check ({why}), so the graph answer remains unverified"
226
249
  + (" — retry" if warming else ""),
227
250
  **({"retry_after_s": 2} if warming else {}),
228
251
  }], "result": str(result["result"]) + (
229
- f"\n\n> Cross-check unavailable: every row above was resolved by name, and "
252
+ f"\n\n> Cross-check unavailable: {graph_problem}, and "
230
253
  f"{why}, so nothing here has been confirmed against a second engine.{nxt}"
231
254
  )}
232
255
  refs = self._reference_lines(str(body))
@@ -236,15 +259,15 @@ class Gateway:
236
259
  merged = (
237
260
  f"{result['result']}\n\n## Cross-check — LSP references to `{target}` "
238
261
  f"({len(refs)})\n"
239
- f"_The rows above were all resolved by NAME by the graph engine. These come from the "
240
- f"language server, which resolves through the file's imports. A caller listed above "
241
- f"but absent here is very likely a name collision; a location here but missing above "
242
- f"is a call the graph could not bind._\n" + listing + more
262
+ f"_The graph answer has a known binding gap: {graph_problem}. These locations come "
263
+ f"from the language server, which resolves the exact definition. A caller listed "
264
+ f"above but absent here is likely a name collision; a location here but missing "
265
+ f"above is a reference the graph could not bind._\n" + listing + more
243
266
  )
244
267
  return {**result, "result": merged, "gaps": [*gaps, {
245
268
  "section": op, "kind": "cross-checked-with-lsp",
246
269
  "engine": "lsp",
247
- "detail": f"every graph row was name-resolved, so the LSP was consulted "
270
+ "detail": f"{graph_problem}, so the LSP was consulted "
248
271
  f"independently and reported {len(refs)} reference location(s); the two "
249
272
  f"lists answer related but different questions and are shown separately",
250
273
  }]}
@@ -67,6 +67,30 @@ _BACKEND_ERROR_MARKERS = (
67
67
  )
68
68
 
69
69
 
70
+ def _split_target_file_hint(target: str) -> tuple[str, str]:
71
+ """Return Serena's symbol pattern and an optional repo-relative file hint.
72
+
73
+ Graph operations accept ``name@path`` to disambiguate duplicate symbols. The gateway may pass
74
+ that exact target to the LSP for an independent reference cross-check, but Serena understands
75
+ only the name-path portion. Keep the syntax aligned here without coupling the LSP provider to
76
+ graph.py's private target parser.
77
+ """
78
+ raw = str(target or "").strip()
79
+ if "@" not in raw:
80
+ return raw, ""
81
+ head, _, tail = raw.rpartition("@")
82
+ if not head.strip() or not tail.strip():
83
+ return raw, ""
84
+ return head.strip(), tail.strip()
85
+
86
+
87
+ def _file_hint_matches(file_path: Any, hint: str) -> bool:
88
+ """Match a file hint on path-segment boundaries, consistently with the graph provider."""
89
+ have = str(file_path or "").replace("\\", "/").strip().strip("/").lower()
90
+ want = str(hint or "").replace("\\", "/").strip().strip("/").lower()
91
+ return bool(have and want and (have == want or have.endswith("/" + want)))
92
+
93
+
70
94
  def _looks_like_backend_error(text: str) -> bool:
71
95
  """Whether *text* is a backend failure message rather than a result.
72
96
 
@@ -773,10 +797,18 @@ class LspProvider:
773
797
  self, session: _LspSession, target: str, root: str, timeout_s: float
774
798
  ) -> str | None:
775
799
  try:
800
+ symbol_target, file_hint = _split_target_file_hint(target)
776
801
  def_out = self._call_tool(
777
802
  session,
778
803
  "find_symbol",
779
- {"name_path_pattern": target, "include_body": True, "max_matches": 5},
804
+ {
805
+ "name_path_pattern": symbol_target,
806
+ "include_body": True,
807
+ # A file hint exists specifically because the name is ambiguous. Retrieve
808
+ # enough definitions to find the requested file instead of silently selecting
809
+ # whichever five Serena happens to return first.
810
+ "max_matches": 50 if file_hint else 5,
811
+ },
780
812
  timeout_s,
781
813
  )
782
814
  if isinstance(def_out, Missing):
@@ -789,6 +821,21 @@ class LspProvider:
789
821
  def_raw = def_out.value
790
822
  def_text = self._extract_text(def_raw)
791
823
  matches = self._loads(def_text)
824
+ if isinstance(matches, list) and file_hint:
825
+ matches = [
826
+ match for match in matches
827
+ if isinstance(match, dict)
828
+ and _file_hint_matches(match.get("relative_path"), file_hint)
829
+ ]
830
+ if not matches:
831
+ # Do not fall through and render the original JSON containing definitions
832
+ # from other files. More importantly, the reference lookup below must remain
833
+ # "not asked", rather than turning an unresolved exact definition into a
834
+ # confident zero-reference answer.
835
+ def_text = (
836
+ f"> The language server found no definition for `{symbol_target}` in "
837
+ f"`{file_hint}`."
838
+ )
792
839
 
793
840
  first: dict | None = None
794
841
  if isinstance(matches, list) and matches:
@@ -163,6 +163,64 @@ def test_lsp_provider_ready_symbol(monkeypatch):
163
163
  assert "parse_result" in r["result"]
164
164
 
165
165
 
166
+ def test_file_qualified_symbol_selects_the_matching_definition(monkeypatch):
167
+ """A graph file hint must survive the LSP cross-check instead of selecting a same-name method."""
168
+ monkeypatch.setattr("codeintel.providers.lsp.shutil.which", lambda x: "/fake/uvx")
169
+ p = LspProvider()
170
+ p._sessions["/my/repo"] = _make_fake_session(_State.READY)
171
+ calls: list[tuple[str, dict]] = []
172
+
173
+ def _fake_call_tool(session, tool, args, timeout_s):
174
+ calls.append((tool, args))
175
+ if tool == "find_symbol":
176
+ return Ok('[{"name_path":"Other/createSession","kind":"Method",'
177
+ '"relative_path":"src/other.ts","body_location":{"start_line":1,'
178
+ '"end_line":2},"body":"wrong"},{"name_path":"WsSessionHandler/createSession",'
179
+ '"kind":"Method","relative_path":"backend/src/session.handler.ts",'
180
+ '"body_location":{"start_line":138,"end_line":177},"body":"right"}]')
181
+ if tool == "find_referencing_symbols":
182
+ assert args["name_path"] == "WsSessionHandler/createSession"
183
+ assert args["relative_path"] == "backend/src/session.handler.ts"
184
+ return Ok('{"backend/src/gateway.ts":{"Method":[{"name_path":"Gateway/routeMessage",'
185
+ '"content_around_reference":"> 612: await handler.createSession()"}]}}')
186
+ return Missing("backend-error", "unstubbed tool")
187
+
188
+ monkeypatch.setattr(p, "_call_tool", _fake_call_tool)
189
+ r = p.build_result(
190
+ "symbol", "createSession@backend/src/session.handler.ts", [], 30000, "/my/repo"
191
+ )
192
+
193
+ assert r["confidence"] == "complete"
194
+ assert "right" in r["result"] and "wrong" not in r["result"]
195
+ assert "backend/src/gateway.ts:613" in r["result"]
196
+ assert calls[0][1]["name_path_pattern"] == "createSession"
197
+ assert calls[0][1]["max_matches"] == 50
198
+
199
+
200
+ def test_file_qualified_symbol_does_not_use_a_definition_from_another_file(monkeypatch):
201
+ monkeypatch.setattr("codeintel.providers.lsp.shutil.which", lambda x: "/fake/uvx")
202
+ p = LspProvider()
203
+ p._sessions["/my/repo"] = _make_fake_session(_State.READY)
204
+ calls: list[str] = []
205
+
206
+ def _fake_call_tool(session, tool, args, timeout_s):
207
+ calls.append(tool)
208
+ return Ok('[{"name_path":"Other/createSession","kind":"Method",'
209
+ '"relative_path":"src/other.ts","body_location":{"start_line":1,'
210
+ '"end_line":2},"body":"wrong"}]')
211
+
212
+ monkeypatch.setattr(p, "_call_tool", _fake_call_tool)
213
+ r = p.build_result(
214
+ "symbol", "createSession@backend/src/session.handler.ts", [], 30000, "/my/repo"
215
+ )
216
+
217
+ assert r["confidence"] == "partial"
218
+ assert "found no definition" in r["result"] and "wrong" not in r["result"]
219
+ assert "References — not retrieved" in r["result"]
220
+ assert calls == ["find_symbol"]
221
+ assert any(g["section"] == "references" and g["kind"] == "not-asked" for g in r["gaps"])
222
+
223
+
166
224
  # ---------------------------------------------------------------------------
167
225
  # Group 8 — Unsupported op when READY
168
226
  # ---------------------------------------------------------------------------
@@ -150,6 +150,70 @@ def test_an_all_guesses_answer_is_cross_checked_against_the_lsp():
150
150
  assert any(g["kind"] == "cross-checked-with-lsp" for g in env["gaps"]), env["gaps"]
151
151
 
152
152
 
153
+ def test_a_file_qualified_graph_miss_is_cross_checked_against_the_lsp():
154
+ target = "createSession@backend/src/session.handler.ts"
155
+ graph = {
156
+ "ok": True,
157
+ "op": "callers",
158
+ "target": target,
159
+ "engine": "graph",
160
+ "result": "## Callers\nNo matching file-qualified symbol has callers in this index.",
161
+ "confidence": "partial",
162
+ "gaps": [{
163
+ "section": "callers",
164
+ "kind": "target-hint-unmatched",
165
+ "detail": "the exact symbol has no bound caller edge",
166
+ }],
167
+ }
168
+ lsp = {
169
+ "ok": True,
170
+ "result": f"## Symbol: {target}\n**Method** — backend/src/session.handler.ts:139\n"
171
+ "```\nbody\n```\n\n## References (1)\n"
172
+ "- backend/src/gateway.ts:613 (Gateway/routeMessage)\n",
173
+ "engine": "lsp",
174
+ "confidence": "complete",
175
+ }
176
+ calls: list[tuple[str, str]] = []
177
+
178
+ class _RecordingLsp(_StubLsp):
179
+ def build_result(self, op, asked_target, files, budget, project_root, **kw):
180
+ calls.append((op, asked_target))
181
+ return super().build_result(op, asked_target, files, budget, project_root, **kw)
182
+
183
+ gw = Gateway(graph=_StubGraph(graph), lsp=_RecordingLsp(lsp))
184
+ env = gw.query("callers", target, engine="auto", project_root=ROOT, budget=30000)
185
+
186
+ assert calls == [("symbol", target)]
187
+ assert "backend/src/gateway.ts:613" in env["result"]
188
+ assert "file-qualified symbol" in env["result"]
189
+ assert any(g["kind"] == "cross-checked-with-lsp" for g in env["gaps"])
190
+
191
+
192
+ def test_an_unresolved_lsp_definition_does_not_claim_zero_references():
193
+ target = "createSession@backend/src/session.handler.ts"
194
+ graph = {
195
+ "ok": True, "op": "callers", "target": target, "engine": "graph",
196
+ "result": "No bound graph caller.", "confidence": "partial",
197
+ "gaps": [{"section": "callers", "kind": "target-hint-unmatched"}],
198
+ }
199
+ lsp = {
200
+ "ok": True, "engine": "lsp", "confidence": "partial",
201
+ "result": "## Symbol\nNo matching definition.\n\n## References — not retrieved",
202
+ "gaps": [{
203
+ "section": "references", "kind": "not-asked",
204
+ "detail": "the exact file-qualified definition was not resolved",
205
+ }],
206
+ }
207
+
208
+ env = Gateway(graph=_StubGraph(graph), lsp=_StubLsp(lsp)).query(
209
+ "callers", target, engine="auto", project_root=ROOT, budget=30000
210
+ )
211
+
212
+ assert "Cross-check unavailable" in env["result"]
213
+ assert "(no references reported)" not in env["result"]
214
+ assert any(g["kind"] == "cross-check-unavailable" for g in env["gaps"])
215
+
216
+
153
217
  def test_a_healthy_graph_answer_is_never_sent_to_the_lsp():
154
218
  """The escalation is for the collision signature only. Firing it on ordinary answers would
155
219
  double every callers query's cost for nothing."""
@@ -1 +0,0 @@
1
- __version__ = "0.23.2"
File without changes
File without changes
File without changes
File without changes