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.
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/PKG-INFO +1 -1
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/pyproject.toml +3 -1
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/Cargo.lock +1 -1
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/Cargo.toml +3 -1
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lang_go.py +140 -46
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/main.py +131 -40
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/repo_map.py +145 -14
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_dense.py +16 -3
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/LICENSE +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/NOTICE +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/README.md +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/deny.toml +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/rust-toolchain.toml +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_ast.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_ast_workflow.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_cpu.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/backend_gpu.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/bin/tg_search_fast.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/cli.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/crossover.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/editor_plane.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/gpu_native.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/index.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/lib.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/main.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/mmap_arrow.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/native_search.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/python_sidecar.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/rg_passthrough.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/routing.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/src/runtime_paths.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_backend.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_cache.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_grep_embed.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_ast_rewrite.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_error_handling.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_graphs.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_integration.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_long_lines.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_gpu_native_streams.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_index.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_mmap_arrow.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_native_search.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_output.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_public_native_cli_parity.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_python_bootstrap.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_release_profile.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_replace.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_resident_worker.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_routing.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_runtime_path_resolution.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_schema_compat.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_search.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_search_golden.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_sidecar_ipc.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/rust_core/tests/test_smart_routing.rs +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/__init__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/__main__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/__init__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/ast_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/ast_wrapper_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/base.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/cpu_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/cudf_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/cybert_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/ripgrep_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/rust_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/stringzilla_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/backends/torch_backend.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/__init__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/_index_lock.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/agent_capsule.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/apply_policy.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/ast_workflows.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/audit_manifest.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/bootstrap.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/checkpoint_store.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/commands.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/docs_coverage.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/dogfood.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/__init__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/base.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/csv_fmt.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/json_fmt.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/ripgrep_fmt.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/formatters/table_fmt.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/inventory.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lang_registry.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lsp_external_provider.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lsp_provider_setup.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/lsp_server.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/mcp_server.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/orient_capsule.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/progress.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/rg_contract.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/rule_packs.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/runtime_paths.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/scan_guardrails.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/session_daemon.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/session_store.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/cli/subprocess_policy.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/__init__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/config.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/__init__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/device_detect.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/device_inventory.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/hardware/memory_manager.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/observability.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/pipeline.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/query_analyzer.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/registration_check.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/reranker.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/result.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_bm25.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_chunker.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_fusion.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_lexical.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/retrieval_scoring.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/core/semantic_index.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/__init__.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/base.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/directory_scanner.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_cudf.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_dstorage.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_fallback.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/io/reader_kvikio.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/perf_guard.py +0 -0
- {tensor_grep-1.45.0 → tensor_grep-1.45.2}/src/tensor_grep/sidecar.py +0 -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.
|
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "tensor_grep_rs"
|
|
3
|
-
version = "1.45.
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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":
|
|
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
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
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,
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
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
|
|
6665
|
-
|
|
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
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
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")
|