tensor-grep 1.45.0__tar.gz → 1.45.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.

Potentially problematic release.


This version of tensor-grep might be problematic. Click here for more details.

Files changed (129) hide show
  1. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/PKG-INFO +1 -1
  2. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/pyproject.toml +3 -1
  3. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/Cargo.lock +1 -1
  4. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/Cargo.toml +3 -1
  5. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lang_go.py +140 -46
  6. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/main.py +131 -40
  7. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/repo_map.py +145 -14
  8. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_dense.py +16 -3
  9. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/LICENSE +0 -0
  10. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/NOTICE +0 -0
  11. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/README.md +0 -0
  12. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/deny.toml +0 -0
  13. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/rust-toolchain.toml +0 -0
  14. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_ast.rs +0 -0
  15. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_ast_workflow.rs +0 -0
  16. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_cpu.rs +0 -0
  17. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_gpu.rs +0 -0
  18. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/bin/tg_search_fast.rs +0 -0
  19. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/cli.rs +0 -0
  20. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/crossover.rs +0 -0
  21. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/editor_plane.rs +0 -0
  22. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/gpu_native.rs +0 -0
  23. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/index.rs +0 -0
  24. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/lib.rs +0 -0
  25. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/main.rs +0 -0
  26. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/mmap_arrow.rs +0 -0
  27. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/native_search.rs +0 -0
  28. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/python_sidecar.rs +0 -0
  29. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/rg_passthrough.rs +0 -0
  30. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/routing.rs +0 -0
  31. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/runtime_paths.rs +0 -0
  32. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_backend.rs +0 -0
  33. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_cache.rs +0 -0
  34. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_grep_embed.rs +0 -0
  35. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_rewrite.rs +0 -0
  36. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native.rs +0 -0
  37. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_error_handling.rs +0 -0
  38. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_graphs.rs +0 -0
  39. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_integration.rs +0 -0
  40. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_long_lines.rs +0 -0
  41. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_streams.rs +0 -0
  42. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_index.rs +0 -0
  43. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_mmap_arrow.rs +0 -0
  44. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_native_search.rs +0 -0
  45. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_output.rs +0 -0
  46. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_public_native_cli_parity.rs +0 -0
  47. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_python_bootstrap.rs +0 -0
  48. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_release_profile.rs +0 -0
  49. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_replace.rs +0 -0
  50. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_resident_worker.rs +0 -0
  51. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_routing.rs +0 -0
  52. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_runtime_path_resolution.rs +0 -0
  53. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_schema_compat.rs +0 -0
  54. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_search.rs +0 -0
  55. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_search_golden.rs +0 -0
  56. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_sidecar_ipc.rs +0 -0
  57. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_smart_routing.rs +0 -0
  58. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/__init__.py +0 -0
  59. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/__main__.py +0 -0
  60. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/__init__.py +0 -0
  61. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/ast_backend.py +0 -0
  62. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/ast_wrapper_backend.py +0 -0
  63. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/base.py +0 -0
  64. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/cpu_backend.py +0 -0
  65. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/cudf_backend.py +0 -0
  66. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/cybert_backend.py +0 -0
  67. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/ripgrep_backend.py +0 -0
  68. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/rust_backend.py +0 -0
  69. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/stringzilla_backend.py +0 -0
  70. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/torch_backend.py +0 -0
  71. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/__init__.py +0 -0
  72. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/_index_lock.py +0 -0
  73. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/agent_capsule.py +0 -0
  74. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/apply_policy.py +0 -0
  75. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/ast_workflows.py +0 -0
  76. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/audit_manifest.py +0 -0
  77. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/bootstrap.py +0 -0
  78. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/checkpoint_store.py +0 -0
  79. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/commands.py +0 -0
  80. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/docs_coverage.py +0 -0
  81. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/dogfood.py +0 -0
  82. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/__init__.py +0 -0
  83. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/base.py +0 -0
  84. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/csv_fmt.py +0 -0
  85. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/json_fmt.py +0 -0
  86. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/ripgrep_fmt.py +0 -0
  87. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/table_fmt.py +0 -0
  88. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/inventory.py +0 -0
  89. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lang_registry.py +0 -0
  90. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lsp_external_provider.py +0 -0
  91. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lsp_provider_setup.py +0 -0
  92. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lsp_server.py +0 -0
  93. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/mcp_server.py +0 -0
  94. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/orient_capsule.py +0 -0
  95. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/progress.py +0 -0
  96. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/rg_contract.py +0 -0
  97. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/rule_packs.py +0 -0
  98. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/runtime_paths.py +0 -0
  99. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/scan_guardrails.py +0 -0
  100. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/session_daemon.py +0 -0
  101. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/session_store.py +0 -0
  102. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/subprocess_policy.py +0 -0
  103. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/__init__.py +0 -0
  104. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/config.py +0 -0
  105. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/__init__.py +0 -0
  106. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/device_detect.py +0 -0
  107. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/device_inventory.py +0 -0
  108. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/memory_manager.py +0 -0
  109. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/observability.py +0 -0
  110. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/pipeline.py +0 -0
  111. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/query_analyzer.py +0 -0
  112. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/registration_check.py +0 -0
  113. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/reranker.py +0 -0
  114. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/result.py +0 -0
  115. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_bm25.py +0 -0
  116. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_chunker.py +0 -0
  117. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_fusion.py +0 -0
  118. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_lexical.py +0 -0
  119. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_scoring.py +0 -0
  120. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/semantic_index.py +0 -0
  121. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/__init__.py +0 -0
  122. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/base.py +0 -0
  123. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/directory_scanner.py +0 -0
  124. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_cudf.py +0 -0
  125. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_dstorage.py +0 -0
  126. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_fallback.py +0 -0
  127. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_kvikio.py +0 -0
  128. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/perf_guard.py +0 -0
  129. {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/sidecar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensor-grep
3
- Version: 1.45.0
3
+ Version: 1.45.2
4
4
  Requires-Dist: typer>=0.12,<0.26
5
5
  Requires-Dist: rich>=13.0
6
6
  Requires-Dist: mcp>=1.2.0
@@ -397,12 +397,14 @@ version_variables = [
397
397
 
398
398
 
399
399
 
400
+
401
+
400
402
 
401
403
 
402
404
 
403
405
  [project]
404
406
  name = "tensor-grep"
405
- version = "1.45.0"
407
+ version = "1.45.2"
406
408
  description = "High-performance grep-compatible CLI with Rust, GPU, and AST/NLP routing"
407
409
  readme = { file = "rust_core/README.md", content-type = "text/markdown" }
408
410
  requires-python = ">=3.11"
@@ -1541,7 +1541,7 @@ dependencies = [
1541
1541
 
1542
1542
  [[package]]
1543
1543
  name = "tensor_grep_rs"
1544
- version = "1.45.0"
1544
+ version = "1.45.2"
1545
1545
  dependencies = [
1546
1546
  "aho-corasick",
1547
1547
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tensor_grep_rs"
3
- version = "1.45.0"
3
+ version = "1.45.2"
4
4
  edition = "2021"
5
5
  description = "Rust core engine for tensor-grep"
6
6
  license = "Apache-2.0"
@@ -304,6 +304,8 @@ path = "src/bin/tg_search_fast.rs"
304
304
 
305
305
 
306
306
 
307
+
308
+
307
309
 
308
310
 
309
311
 
@@ -124,7 +124,15 @@ _GO_DEF_NODE_KINDS = (
124
124
 
125
125
 
126
126
  def _go_receiver_type_name(method_node: Any, source_bytes: bytes) -> str | None:
127
- """Return the (possibly pointer) receiver type name for a ``method_declaration`` node."""
127
+ """Return the (possibly pointer, possibly generic) receiver type name for a
128
+ ``method_declaration`` node.
129
+
130
+ F8 fix: a GENERIC receiver (``func (r *MyType[T]) M()`` / ``func (r MyType[T]) N()``) parses
131
+ the receiver's type as a ``generic_type`` node whose own text is ``"MyType[T]"`` -- that never
132
+ matches the plain ``"MyType"`` name a ``type_spec`` declares, so method<->type association
133
+ silently broke for every generic Go receiver. Descending to ``generic_type``'s ``type`` field
134
+ (the base ``type_identifier``) restores the plain base name.
135
+ """
128
136
  receiver = method_node.child_by_field_name("receiver")
129
137
  if receiver is None:
130
138
  return None
@@ -144,9 +152,43 @@ def _go_receiver_type_name(method_node: Any, source_bytes: bytes) -> str | None:
144
152
  )
145
153
  if inner is not None:
146
154
  type_node = inner
155
+ if type_node.type == "generic_type":
156
+ base_type_node = type_node.child_by_field_name("type")
157
+ if base_type_node is not None:
158
+ type_node = base_type_node
147
159
  return _tree_sitter_node_text(source_bytes, type_node)
148
160
 
149
161
 
162
+ def _go_import_spec_path_text(path_field: Any, source_bytes: bytes) -> str | None:
163
+ """Return an ``import_spec``'s string-literal import path (quotes stripped), or ``None``.
164
+
165
+ F11 fix: the primary route reads the ``interpreted_string_literal_content`` child that recent
166
+ ``tree_sitter_go`` grammar versions expose. An older/differently-built grammar package can
167
+ omit that child (there is no dedicated content node at all, or it is named differently) --
168
+ when that happens this used to silently return no path for EVERY import in the file, which
169
+ then silently degrades all cross-package import resolution with no error and no
170
+ ``resolution_gaps`` entry (the parser loaded fine, so nothing marks a gap). Falling back to
171
+ quote-stripping the string literal's own raw text keeps import extraction working even
172
+ against a grammar shape this module wasn't written against.
173
+ """
174
+ if path_field is None:
175
+ return None
176
+ content_node = next(
177
+ (
178
+ child
179
+ for child in path_field.children
180
+ if child.type == "interpreted_string_literal_content"
181
+ ),
182
+ None,
183
+ )
184
+ if content_node is not None:
185
+ return _tree_sitter_node_text(source_bytes, content_node)
186
+ raw_text = _tree_sitter_node_text(source_bytes, path_field).strip()
187
+ if len(raw_text) >= 2 and raw_text[0] in {'"', "`"} and raw_text[-1] == raw_text[0]:
188
+ return raw_text[1:-1]
189
+ return None
190
+
191
+
150
192
  def go_imports_and_symbols(path: Path) -> tuple[list[str], list[dict[str, Any]]]:
151
193
  """Extract import paths + top-level definitions from a Go source file, one AST pass.
152
194
 
@@ -184,20 +226,9 @@ def go_imports_and_symbols(path: Path) -> tuple[list[str], list[dict[str, Any]]]
184
226
  node_type = node.type
185
227
  if node_type == "import_spec":
186
228
  path_field = node.child_by_field_name("path")
187
- content_node = (
188
- next(
189
- (
190
- child
191
- for child in path_field.children
192
- if child.type == "interpreted_string_literal_content"
193
- ),
194
- None,
195
- )
196
- if path_field is not None
197
- else None
198
- )
199
- if content_node is not None:
200
- imports.append(_node_text(content_node))
229
+ import_path_text = _go_import_spec_path_text(path_field, source_bytes)
230
+ if import_path_text is not None:
231
+ imports.append(import_path_text)
201
232
  elif node_type == "function_declaration":
202
233
  name_node = node.child_by_field_name("name")
203
234
  if name_node is not None:
@@ -437,7 +468,16 @@ def _go_repo_context(repo_root: Path | str | None) -> dict[str, Any]:
437
468
  def _go_import_path_to_dir(import_path: str, context: dict[str, Any]) -> Path | None:
438
469
  """Resolve an import path (e.g. ``"example.com/mod/bar"``) to an absolute directory via the
439
470
  longest matching ``module_dirs`` prefix (longest-prefix-wins handles a workspace where one
440
- module's path is itself a prefix of another's)."""
471
+ module's path is itself a prefix of another's).
472
+
473
+ F24 fix: stop at an intervening ``go.mod`` boundary. A nested module (its own ``go.mod`` that
474
+ is NOT listed as a ``go.work`` ``use`` entry -- e.g. simply forgotten) is a SEPARATE module
475
+ even when the enclosing module's path prefix happens to textually match its subdirectory --
476
+ walking past that boundary used to silently resolve a cross-package reference into the wrong
477
+ module's directory. Any ``go.mod`` strictly under the matched prefix's own root (which is
478
+ expected and ignored -- that root's ``go.mod`` is exactly what got this prefix primed) now
479
+ aborts resolution (returns ``None``) instead of fabricating a cross-module match.
480
+ """
441
481
  module_dirs = context.get("module_dirs", {})
442
482
  if not isinstance(module_dirs, dict):
443
483
  return None
@@ -448,9 +488,16 @@ def _go_import_path_to_dir(import_path: str, context: dict[str, Any]) -> Path |
448
488
  best_prefix = prefix
449
489
  if best_prefix is None:
450
490
  return None
451
- base_dir = Path(str(module_dirs[best_prefix]))
491
+ base_dir = Path(str(module_dirs[best_prefix])).resolve()
452
492
  remainder = import_path[len(best_prefix) :].lstrip("/")
453
- return (base_dir / remainder).resolve() if remainder else base_dir.resolve()
493
+ if not remainder:
494
+ return base_dir
495
+ current = base_dir
496
+ for part in remainder.split("/"):
497
+ current = current / part
498
+ if (current / "go.mod").is_file():
499
+ return None
500
+ return current
454
501
 
455
502
 
456
503
  def _go_import_bindings(source_bytes: bytes, tree: Any) -> list[dict[str, Any]]:
@@ -461,19 +508,8 @@ def _go_import_bindings(source_bytes: bytes, tree: Any) -> list[dict[str, Any]]:
461
508
  if node.type == "import_spec":
462
509
  path_field = node.child_by_field_name("path")
463
510
  name_field = node.child_by_field_name("name")
464
- content_node = (
465
- next(
466
- (
467
- child
468
- for child in path_field.children
469
- if child.type == "interpreted_string_literal_content"
470
- ),
471
- None,
472
- )
473
- if path_field is not None
474
- else None
475
- )
476
- if content_node is not None:
511
+ import_path_text = _go_import_spec_path_text(path_field, source_bytes)
512
+ if import_path_text is not None:
477
513
  alias: str | None = None
478
514
  dot = False
479
515
  blank = False
@@ -485,7 +521,7 @@ def _go_import_bindings(source_bytes: bytes, tree: Any) -> list[dict[str, Any]]:
485
521
  elif name_field.type == "blank_identifier":
486
522
  blank = True
487
523
  bindings.append({
488
- "path": _tree_sitter_node_text(source_bytes, content_node),
524
+ "path": import_path_text,
489
525
  "alias": alias,
490
526
  "dot": dot,
491
527
  "blank": blank,
@@ -571,10 +607,38 @@ _GO_NAME_DEFINING_PARENT_TYPES = {
571
607
  }
572
608
 
573
609
 
610
+ def _go_package_defines_function(target_dir: str, name: str) -> bool:
611
+ """F10 fallback confirmation: True iff *target_dir* (a resolved package directory) contains a
612
+ top-level ``function``/``method`` declaration named *name*.
613
+
614
+ Used only when ``go_references_and_calls`` is not given ``definition_dirs`` (the stronger,
615
+ definition-aware check used by repo_map.py's refs/callers scan -- see F25 in that function's
616
+ docstring). Deliberately un-cached: this module never caches anything that reads file content
617
+ without an mtime guard (see the module-level caches above, which are keyed on repo ROOT
618
+ priming, not file content), and a Go package directory is small enough that a plain per-call
619
+ scan is cheap.
620
+ """
621
+ directory = Path(target_dir)
622
+ if not directory.is_dir():
623
+ return False
624
+ try:
625
+ entries = sorted(directory.glob("*.go"))
626
+ except OSError:
627
+ return False
628
+ for entry in entries:
629
+ _, symbols = go_imports_and_symbols(entry)
630
+ for record in symbols:
631
+ if record.get("name") == name and record.get("kind") in {"function", "method"}:
632
+ return True
633
+ return False
634
+
635
+
574
636
  def go_references_and_calls(
575
637
  path: Path,
576
638
  symbol: str,
577
639
  repo_root: Path | str | None = None,
640
+ *,
641
+ definition_dirs: frozenset[str] | None = None,
578
642
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
579
643
  """Reference/call rows for *symbol* in *path*.
580
644
 
@@ -585,15 +649,30 @@ def go_references_and_calls(
585
649
  *field*/method-selector mention is ``field_identifier``.
586
650
 
587
651
  Package-qualified access (``pkg.Symbol``) resolves through this file's import bindings +
588
- the primed repo context: if the qualifying identifier is a recognized import alias whose
589
- resolved directory matches *definition_path*'s directory, the row carries
590
- ``resolution_provenance=["go-import-resolution"]`` at ``resolution_confidence=0.95``. A
591
- selector call whose left-hand operand is NOT a recognized package alias (e.g. ``w.Write(x)``
592
- where ``w`` is an arbitrary receiver variable of unknown static type) is equifinal -- ANY
593
- type with a same-named method would match textually -- so it is still emitted (never
594
- silently dropped) but capped at ``resolution_confidence<=0.7`` with
595
- ``resolution_provenance=["receiver-heuristic"]``, per the Stage 1 no-fabricated-precision
596
- trap.
652
+ the primed repo context. Two shapes:
653
+
654
+ - **Non-call selector** (e.g. ``config.DefaultTimeout``): F9 fix -- classified ``ref_kind``
655
+ "value" (a package-qualified const/var read), not "field", whenever the operand resolves as
656
+ a recognized import alias; an unresolved operand (e.g. a genuine struct field access) still
657
+ classifies "field" as before.
658
+ - **Call** (e.g. ``pkg.Helper(...)`` / ``w.Write(...)``): a resolved package alias only earns
659
+ ``resolution_provenance=["go-import-resolution"]`` at ``resolution_confidence=0.95`` if the
660
+ resolved package is CONFIRMED to actually own *symbol* -- F25 fix: when *definition_dirs* is
661
+ supplied (repo_map.py always supplies it, built from the symbol's own known definitions),
662
+ confirmation means the resolved directory is one of those definition dirs, which also fixes
663
+ a same-named-export collision (two unrelated packages each exporting a symbol with the same
664
+ name used to both get 0.95 turn just because the alias itself resolved); when
665
+ *definition_dirs* is omitted (e.g. a standalone caller), F10 fallback confirmation checks
666
+ whether the resolved package directory itself defines a top-level function/method named
667
+ *symbol* -- this is what prevents a LOCAL VARIABLE that happens to share its name with an
668
+ unrelated import alias (``w := SomeStruct{}`` shadowing `import w "pkg/w"`) from fabricating
669
+ high confidence for ``w.Method()`` when ``pkg/w`` does not actually define ``Method``.
670
+ Either way, an unconfirmed resolution is never dropped -- it demotes to the same
671
+ ``resolution_confidence<=0.7`` / ``resolution_provenance=["receiver-heuristic"]`` band as a
672
+ selector call whose left-hand operand is not a recognized package alias at all (e.g.
673
+ ``w.Write(x)`` where ``w`` is an arbitrary receiver variable of unknown static type) --
674
+ equifinal in both cases (ANY type with a same-named method could match textually), per the
675
+ Stage 1 no-fabricated-precision trap.
597
676
  """
598
677
  if path.suffix != ".go":
599
678
  return [], []
@@ -643,6 +722,21 @@ def go_references_and_calls(
643
722
  return None
644
723
  return {"provenance": ["go-import-resolution"], "confidence": 0.95, "dir": target_dir}
645
724
 
725
+ def _confident_call_resolution(package_resolution: dict[str, Any] | None) -> dict[str, Any]:
726
+ """F10/F25 fix: only trust ``package_resolution`` (0.95) for a CALL when the resolved
727
+ package is confirmed to actually own *symbol* -- see the ``go_references_and_calls``
728
+ docstring for the two confirmation routes. Anything unconfirmed demotes to the same
729
+ equifinal receiver-heuristic band a non-package selector call already gets, never drops.
730
+ """
731
+ if package_resolution is not None:
732
+ target_dir = str(package_resolution.get("dir", ""))
733
+ if definition_dirs is not None:
734
+ if target_dir in definition_dirs:
735
+ return package_resolution
736
+ elif _go_package_defines_function(target_dir, symbol):
737
+ return package_resolution
738
+ return {"provenance": ["receiver-heuristic"], "confidence": 0.7}
739
+
646
740
  def _emit(
647
741
  bucket: list[dict[str, Any]],
648
742
  node: Any,
@@ -715,10 +809,7 @@ def go_references_and_calls(
715
809
  and grandparent.child_by_field_name("function") == parent
716
810
  )
717
811
  if is_call:
718
- resolution = package_resolution or {
719
- "provenance": ["receiver-heuristic"],
720
- "confidence": 0.7,
721
- }
812
+ resolution = _confident_call_resolution(package_resolution)
722
813
  _emit(
723
814
  references,
724
815
  node,
@@ -728,11 +819,14 @@ def go_references_and_calls(
728
819
  )
729
820
  _emit(calls, node, kind="call", ref_kind="call", resolution=resolution)
730
821
  else:
822
+ # F9 fix: a package-qualified non-call selector (`config.DefaultTimeout`)
823
+ # is a VALUE read (const/var), not a struct FIELD access -- only a
824
+ # genuinely unresolved operand (a real struct field access) stays "field".
731
825
  _emit(
732
826
  references,
733
827
  node,
734
828
  kind="reference",
735
- ref_kind="field",
829
+ ref_kind="value" if package_resolution is not None else "field",
736
830
  resolution=package_resolution,
737
831
  )
738
832
  for child in node.children:
@@ -44,6 +44,7 @@ from tensor_grep.cli.runtime_paths import (
44
44
  from tensor_grep.cli.scan_guardrails import BroadScanRefusedError, ensure_scan_not_broad
45
45
  from tensor_grep.core.observability import nvtx_range
46
46
  from tensor_grep.core.result import MatchLine
47
+ from tensor_grep.core.retrieval_chunker import MAX_CHUNKS
47
48
  from tensor_grep.io.directory_scanner import UNBOUNDED_VENDORED_ROOT_DIR_NAMES
48
49
  from tensor_grep.sidecar import DEFAULT_CLASSIFY_MAX_LINES
49
50
 
@@ -3503,18 +3504,47 @@ def _search_with_cpu_fallback(
3503
3504
  return CPUBackend().search(current_file, pattern, config=config)
3504
3505
 
3505
3506
 
3507
+ # F5 (Fable audit MED): retrieval_chunker.MAX_CHUNKS bounds a single chunk_file() call (per FILE).
3508
+ # A matched-file set of many small files can still blow past a sane CORPUS-wide total even though
3509
+ # no single file trips the per-file guard, so DenseIndex.__init__'s single-batch encode would face
3510
+ # unbounded memory. Cap the CORPUS total here too, sharing the same threshold as the per-file guard
3511
+ # (no separate magic number to keep in sync).
3512
+ _SEMANTIC_CORPUS_CHUNK_CAP = MAX_CHUNKS
3513
+
3514
+
3515
+ def _set_semantic_rank_fallback_reason(all_results: "SearchResult") -> None:
3516
+ """Probe dense-leg availability and set ``rank_fallback_reason`` (F16, Fable audit LOW).
3517
+
3518
+ Used for the 0-match `--semantic` case: with no matches there is nothing to rerank, so the
3519
+ full :func:`_apply_semantic_rerank` path (chunking, model load) is skipped entirely -- but the
3520
+ availability probe must still run so the JSON envelope stays honest (a dense-unavailable
3521
+ search must report that even when it happens to find zero matches, not silently omit
3522
+ ``rank_fallback_reason``).
3523
+ """
3524
+ from tensor_grep.core.retrieval_dense import dense_available
3525
+
3526
+ available, unavailable_reason = dense_available()
3527
+ if not available:
3528
+ all_results.rank_fallback_reason = unavailable_reason
3529
+ sys.stderr.write(f"tg: {unavailable_reason}\n")
3530
+
3531
+
3506
3532
  def _apply_semantic_rerank(all_results: "SearchResult", pattern: str) -> "SearchResult":
3507
3533
  """Apply the `--semantic` hybrid (BM25 + dense RRF) rerank, fail-closed to BM25-only.
3508
3534
 
3509
3535
  The dense leg is best-effort: when the `semantic` extra is absent, the model has not been
3510
- fetched, or the model produces a malformed/mismatched embedding, this degrades VISIBLY to a
3511
- BM25-only rerank (stderr warning + ``rank_fallback_reason`` set) -- it never silently returns
3512
- unranked output and never mislabels BM25-only output as "semantic". A genuine backend fault
3513
- (e.g. a corrupt model directory) raises ``BackendExecutionError`` instead of degrading, per
3514
- the Backend Fail-Closed Contract -- that is NOT caught here.
3536
+ fetched, the model produces a malformed/mismatched embedding, or a query-time dense fault
3537
+ occurs (F1: e.g. a dim mismatch raised from inside `rerank_hybrid`'s call to
3538
+ `DenseIndex.query`), this degrades VISIBLY to a BM25-only rerank (stderr warning +
3539
+ ``rank_fallback_reason`` set) -- it never silently returns unranked output and never mislabels
3540
+ BM25-only output as "semantic". A genuine backend fault (e.g. a corrupt model directory)
3541
+ raises ``BackendExecutionError`` instead of degrading, per the Backend Fail-Closed Contract --
3542
+ that is NOT caught here; the caller (the `search` command) must catch it and exit cleanly
3543
+ (F4).
3515
3544
  """
3516
3545
  from tensor_grep.core.reranker import rerank_hybrid
3517
- from tensor_grep.core.retrieval_chunker import chunk_file
3546
+ from tensor_grep.core.retrieval_bm25 import Bm25Index
3547
+ from tensor_grep.core.retrieval_chunker import Chunk, chunk_file
3518
3548
  from tensor_grep.core.retrieval_dense import (
3519
3549
  DenseIndex,
3520
3550
  DenseUnavailableError,
@@ -3528,23 +3558,73 @@ def _apply_semantic_rerank(all_results: "SearchResult", pattern: str) -> "Search
3528
3558
  if not available:
3529
3559
  all_results.rank_fallback_reason = unavailable_reason
3530
3560
  sys.stderr.write(f"tg: {unavailable_reason}\n")
3531
- else:
3561
+
3562
+ # F3 (Fable audit MED): build the chunk corpus ONCE and share it between the BM25 and dense
3563
+ # legs. Previously the dense leg's corpus was built here while the BM25 leg rebuilt its own
3564
+ # corpus from scratch inside `rerank_hybrid` (bm25_index=None) -- a second full file-I/O pass,
3565
+ # and a silent RRF-misalignment risk if the two passes' chunk_size/overlap defaults ever
3566
+ # diverge.
3567
+ chunks: list[Chunk] = []
3568
+ for path in all_results.matched_file_paths:
3569
+ try:
3570
+ file_chunks = chunk_file(path)
3571
+ except RuntimeError as exc:
3572
+ # F5: retrieval_chunker's own MAX_CHUNKS guard is PER FILE; a single pathological file
3573
+ # can still trip it on its own even before the corpus-wide cap below is reached. Either
3574
+ # way, degrade to BM25-only (using whatever we already have -- discard the corpus, let
3575
+ # rerank_hybrid rebuild its own BM25-only chunks) rather than crash the whole search.
3576
+ all_results.rank_fallback_reason = str(exc)
3577
+ sys.stderr.write(f"tg: {exc}\n")
3578
+ return rerank_hybrid(
3579
+ all_results, pattern, all_results.matched_file_paths, dense_index=None
3580
+ )
3581
+ chunks.extend(file_chunks)
3582
+ if len(chunks) > _SEMANTIC_CORPUS_CHUNK_CAP:
3583
+ reason = (
3584
+ "semantic ranking unavailable: corpus chunk cap "
3585
+ f"({_SEMANTIC_CORPUS_CHUNK_CAP}) exceeded across the matched file set -- narrow "
3586
+ "the search to fewer files for a semantic rerank"
3587
+ )
3588
+ all_results.rank_fallback_reason = reason
3589
+ sys.stderr.write(f"tg: {reason}\n")
3590
+ return rerank_hybrid(
3591
+ all_results, pattern, all_results.matched_file_paths, dense_index=None
3592
+ )
3593
+
3594
+ bm25_index = Bm25Index(chunks)
3595
+
3596
+ if available:
3532
3597
  try:
3533
3598
  model = load_dense_model(default_model_dir())
3534
- chunks = []
3535
- for path in all_results.matched_file_paths:
3536
- chunks.extend(chunk_file(path))
3537
3599
  dense_index = DenseIndex(chunks, model)
3538
3600
  except DenseUnavailableError as exc:
3539
3601
  all_results.rank_fallback_reason = str(exc)
3540
3602
  sys.stderr.write(f"tg: {exc}\n")
3603
+ # BackendExecutionError (e.g. a corrupt model directory) deliberately propagates: that is
3604
+ # an unrecoverable fault the CLI boundary must catch and exit on (F4), not degrade here.
3541
3605
 
3542
- return rerank_hybrid(
3543
- all_results,
3544
- pattern,
3545
- all_results.matched_file_paths,
3546
- dense_index=dense_index,
3547
- )
3606
+ try:
3607
+ return rerank_hybrid(
3608
+ all_results,
3609
+ pattern,
3610
+ all_results.matched_file_paths,
3611
+ bm25_index=bm25_index,
3612
+ dense_index=dense_index,
3613
+ )
3614
+ except DenseUnavailableError as exc:
3615
+ # F1: a query-time dense fault (e.g. a dim mismatch) is raised from INSIDE rerank_hybrid's
3616
+ # call to `DenseIndex.query`, outside the try/except above (which only guards index
3617
+ # construction). Degrade to BM25-only here too -- reuse the SAME bm25_index (no second
3618
+ # chunk pass) rather than let it traceback.
3619
+ all_results.rank_fallback_reason = str(exc)
3620
+ sys.stderr.write(f"tg: {exc}\n")
3621
+ return rerank_hybrid(
3622
+ all_results,
3623
+ pattern,
3624
+ all_results.matched_file_paths,
3625
+ bm25_index=bm25_index,
3626
+ dense_index=None,
3627
+ )
3548
3628
 
3549
3629
 
3550
3630
  def _search_error_payload(error: str, detail: str) -> dict[str, object]:
@@ -6661,8 +6741,25 @@ def search_command(
6661
6741
  all_results.match_counts_by_file[match.file] = (
6662
6742
  all_results.match_counts_by_file.get(match.file, 0) + 1
6663
6743
  )
6664
- if config.semantic_rank and all_results.matches:
6665
- all_results = _apply_semantic_rerank(all_results, pattern)
6744
+ if config.semantic_rank:
6745
+ if all_results.matches:
6746
+ try:
6747
+ all_results = _apply_semantic_rerank(all_results, pattern)
6748
+ except BackendExecutionError as exc:
6749
+ # F4 (Fable audit MED): a genuine dense-backend fault (e.g. a corrupt model
6750
+ # directory) must exit cleanly with a `tg:` message, never a raw traceback --
6751
+ # `_apply_semantic_rerank` deliberately does NOT catch this (see its docstring);
6752
+ # this is the CLI boundary the Backend Fail-Closed Contract requires.
6753
+ if json:
6754
+ _emit_search_error_json("semantic_backend_error", str(exc))
6755
+ else:
6756
+ typer.echo(f"tg: {exc}", err=True)
6757
+ sys.exit(2)
6758
+ else:
6759
+ # F16 (Fable audit LOW): probe dense-leg availability even on a 0-match search so
6760
+ # `rank_fallback_reason` is set whenever the leg is unavailable, regardless of match
6761
+ # count -- skipping the probe here silently made the JSON envelope dishonest.
6762
+ _set_semantic_rank_fallback_reason(all_results)
6666
6763
  elif config.rank_bm25 and all_results.matches:
6667
6764
  from tensor_grep.core.reranker import rerank_by_bm25
6668
6765
 
@@ -9013,10 +9110,7 @@ def blast_radius_plan(
9013
9110
 
9014
9111
  Like `blast-radius --json` but shaped as an edit/action plan (no source snippets).
9015
9112
  """
9016
- from tensor_grep.cli.repo_map import (
9017
- build_symbol_blast_radius_plan,
9018
- build_symbol_blast_radius_plan_json,
9019
- )
9113
+ from tensor_grep.cli.repo_map import build_symbol_blast_radius_plan
9020
9114
 
9021
9115
  try:
9022
9116
  resolved_path, resolved_symbol = _resolve_path_and_symbol(
@@ -9025,20 +9119,6 @@ def blast_radius_plan(
9025
9119
  symbol_option=symbol,
9026
9120
  command_name="blast-radius-plan",
9027
9121
  )
9028
- if json_output:
9029
- typer.echo(
9030
- build_symbol_blast_radius_plan_json(
9031
- resolved_symbol,
9032
- resolved_path,
9033
- max_depth=max_depth,
9034
- max_files=max_files,
9035
- max_symbols=max_symbols,
9036
- semantic_provider=provider,
9037
- max_repo_files=max_repo_files,
9038
- )
9039
- )
9040
- return
9041
-
9042
9122
  payload = build_symbol_blast_radius_plan(
9043
9123
  resolved_symbol,
9044
9124
  resolved_path,
@@ -9052,10 +9132,21 @@ def blast_radius_plan(
9052
9132
  typer.echo(str(exc), err=True)
9053
9133
  raise typer.Exit(1) from exc
9054
9134
 
9055
- typer.echo(f"Blast radius plan for {payload['symbol']} in {payload['path']}")
9056
- typer.echo(
9057
- f"files={len(payload['files'])} tests={len(payload['tests'])} symbols={len(payload['symbols'])}"
9058
- )
9135
+ # F14 (Fable audit MED): output the payload FIRST, then gate on the shared _scan_incomplete
9136
+ # contract -- mirrors blast-radius/map/context-render/edit-plan/blast-radius-render (Cluster B,
9137
+ # 2026-07-06). This payload is built from build_symbol_blast_radius_from_map and carries the
9138
+ # exact scan_limit/caller_scan_truncated markers the gate checks; without this, a scan-truncated
9139
+ # plan exited 0 while the sibling `blast-radius` command exits 2 on identical truncation.
9140
+ if json_output:
9141
+ typer.echo(json.dumps(payload, indent=2))
9142
+ else:
9143
+ typer.echo(f"Blast radius plan for {payload['symbol']} in {payload['path']}")
9144
+ typer.echo(
9145
+ f"files={len(payload['files'])} tests={len(payload['tests'])} symbols={len(payload['symbols'])}"
9146
+ )
9147
+
9148
+ if _scan_incomplete(payload):
9149
+ raise typer.Exit(2)
9059
9150
 
9060
9151
 
9061
9152
  @session_app.command("open")