tensor-grep 1.76.2__tar.gz → 1.76.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/PKG-INFO +1 -1
  2. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/pyproject.toml +3 -1
  3. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/Cargo.lock +1 -1
  4. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/Cargo.toml +3 -1
  5. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/gpu_native.rs +187 -4
  6. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/main.rs +14 -0
  7. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_gpu_native_graphs.rs +2 -0
  8. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_gpu_native_integration.rs +3 -0
  9. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_gpu_native_long_lines.rs +5 -0
  10. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_gpu_native_streams.rs +4 -0
  11. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/session_daemon.py +87 -4
  12. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/LICENSE +0 -0
  13. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/NOTICE +0 -0
  14. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/README.md +0 -0
  15. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/deny.toml +0 -0
  16. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/rust-toolchain.toml +0 -0
  17. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/backend_ast.rs +0 -0
  18. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/backend_ast_workflow.rs +0 -0
  19. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/backend_cpu.rs +0 -0
  20. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/bin/tg_search_fast.rs +0 -0
  21. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/cli.rs +0 -0
  22. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/crossover.rs +0 -0
  23. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/editor_plane.rs +0 -0
  24. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/index.rs +0 -0
  25. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/index_lock.rs +0 -0
  26. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/lib.rs +0 -0
  27. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/mmap_arrow.rs +0 -0
  28. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/native_search.rs +0 -0
  29. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/python_sidecar.rs +0 -0
  30. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/rg_passthrough.rs +0 -0
  31. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/routing.rs +0 -0
  32. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/src/runtime_paths.rs +0 -0
  33. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_ast_backend.rs +0 -0
  34. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_ast_cache.rs +0 -0
  35. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_ast_grep_embed.rs +0 -0
  36. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_ast_rewrite.rs +0 -0
  37. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_audit_manifest_symlink_unix.rs +0 -0
  38. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_gpu_native.rs +0 -0
  39. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_gpu_native_error_handling.rs +0 -0
  40. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_index.rs +0 -0
  41. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_index_lock_concurrency.rs +0 -0
  42. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_mmap_arrow.rs +0 -0
  43. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_native_search.rs +0 -0
  44. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_output.rs +0 -0
  45. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_public_native_cli_parity.rs +0 -0
  46. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_python_bootstrap.rs +0 -0
  47. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_release_profile.rs +0 -0
  48. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_replace.rs +0 -0
  49. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_resident_worker.rs +0 -0
  50. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_routing.rs +0 -0
  51. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_runtime_path_resolution.rs +0 -0
  52. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_schema_compat.rs +0 -0
  53. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_search.rs +0 -0
  54. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_search_golden.rs +0 -0
  55. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_sidecar_ipc.rs +0 -0
  56. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/rust_core/tests/test_smart_routing.rs +0 -0
  57. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/__init__.py +0 -0
  58. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/__main__.py +0 -0
  59. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/__init__.py +0 -0
  60. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/ast_backend.py +0 -0
  61. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/ast_wrapper_backend.py +0 -0
  62. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/base.py +0 -0
  63. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/cpu_backend.py +0 -0
  64. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/cudf_backend.py +0 -0
  65. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/cybert_backend.py +0 -0
  66. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/ripgrep_backend.py +0 -0
  67. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/rust_backend.py +0 -0
  68. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/stringzilla_backend.py +0 -0
  69. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/backends/torch_backend.py +0 -0
  70. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/__init__.py +0 -0
  71. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/_index_lock.py +0 -0
  72. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/agent_capsule.py +0 -0
  73. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/apply_policy.py +0 -0
  74. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/ast_workflows.py +0 -0
  75. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/audit_manifest.py +0 -0
  76. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/bootstrap.py +0 -0
  77. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/checkpoint_store.py +0 -0
  78. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/codemap.py +0 -0
  79. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/commands.py +0 -0
  80. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/docs_coverage.py +0 -0
  81. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/dogfood.py +0 -0
  82. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/evidence_receipt.py +0 -0
  83. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/evidence_signing.py +0 -0
  84. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/formatters/__init__.py +0 -0
  85. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/formatters/base.py +0 -0
  86. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/formatters/csv_fmt.py +0 -0
  87. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/formatters/json_fmt.py +0 -0
  88. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/formatters/ripgrep_fmt.py +0 -0
  89. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/formatters/table_fmt.py +0 -0
  90. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/inventory.py +0 -0
  91. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/lang_go.py +0 -0
  92. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/lang_registry.py +0 -0
  93. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/lsp_external_provider.py +0 -0
  94. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/lsp_provider_setup.py +0 -0
  95. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/lsp_server.py +0 -0
  96. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/main.py +0 -0
  97. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/mcp_server.py +0 -0
  98. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/orient_capsule.py +0 -0
  99. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/progress.py +0 -0
  100. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/repo_map.py +0 -0
  101. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/rg_contract.py +0 -0
  102. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/rule_packs.py +0 -0
  103. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/runtime_paths.py +0 -0
  104. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/scan_guardrails.py +0 -0
  105. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/session_store.py +0 -0
  106. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/cli/subprocess_policy.py +0 -0
  107. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/__init__.py +0 -0
  108. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/config.py +0 -0
  109. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/hardware/__init__.py +0 -0
  110. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/hardware/device_detect.py +0 -0
  111. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/hardware/device_inventory.py +0 -0
  112. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/hardware/memory_manager.py +0 -0
  113. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/observability.py +0 -0
  114. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/pipeline.py +0 -0
  115. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/query_analyzer.py +0 -0
  116. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/registration_check.py +0 -0
  117. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/reranker.py +0 -0
  118. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/result.py +0 -0
  119. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/retrieval_bm25.py +0 -0
  120. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/retrieval_chunker.py +0 -0
  121. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/retrieval_dense.py +0 -0
  122. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/retrieval_fusion.py +0 -0
  123. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/retrieval_late.py +0 -0
  124. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/retrieval_lexical.py +0 -0
  125. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/retrieval_scoring.py +0 -0
  126. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/core/semantic_index.py +0 -0
  127. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/io/__init__.py +0 -0
  128. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/io/base.py +0 -0
  129. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/io/directory_scanner.py +0 -0
  130. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/io/reader_cudf.py +0 -0
  131. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/io/reader_dstorage.py +0 -0
  132. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/io/reader_fallback.py +0 -0
  133. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/io/reader_kvikio.py +0 -0
  134. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/src/tensor_grep/perf_guard.py +0 -0
  135. {tensor_grep-1.76.2 → tensor_grep-1.76.4}/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.76.2
3
+ Version: 1.76.4
4
4
  Requires-Dist: typer>=0.12,<0.26
5
5
  Requires-Dist: rich>=13.0
6
6
  Requires-Dist: mcp>=1.2.0
@@ -530,12 +530,14 @@ version_variables = [
530
530
 
531
531
 
532
532
 
533
+
534
+
533
535
 
534
536
 
535
537
 
536
538
  [project]
537
539
  name = "tensor-grep"
538
- version = "1.76.2"
540
+ version = "1.76.4"
539
541
  description = "High-performance grep-compatible CLI with Rust, GPU, and AST/NLP routing"
540
542
  readme = { file = "rust_core/README.md", content-type = "text/markdown" }
541
543
  requires-python = ">=3.11"
@@ -1579,7 +1579,7 @@ dependencies = [
1579
1579
 
1580
1580
  [[package]]
1581
1581
  name = "tensor_grep_rs"
1582
- version = "1.76.2"
1582
+ version = "1.76.4"
1583
1583
  dependencies = [
1584
1584
  "aho-corasick",
1585
1585
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tensor_grep_rs"
3
- version = "1.76.2"
3
+ version = "1.76.4"
4
4
  edition = "2021"
5
5
  description = "Rust core engine for tensor-grep"
6
6
  license = "Apache-2.0"
@@ -438,6 +438,8 @@ path = "src/bin/tg_search_fast.rs"
438
438
 
439
439
 
440
440
 
441
+
442
+
441
443
 
442
444
 
443
445
 
@@ -361,6 +361,16 @@ pub struct GpuNativeSearchConfig {
361
361
  pub hidden: bool,
362
362
  pub max_depth: Option<usize>,
363
363
  pub max_batch_bytes: Option<usize>,
364
+ /// Whether the caller omitted an explicit PATH (the search root defaulted to `.` rather than
365
+ /// a user-supplied path). Gates `check_gpu_native_implicit_walk_ceiling`, this engine's own
366
+ /// refuse-before-enumerate guard (audit #109 -- the cuda-native-GPU sibling of
367
+ /// `NativeSearchConfig::path_was_implicit`, audit #105, itself the native-CPU sibling of
368
+ /// `RipgrepSearchArgs::path_was_implicit`, audit #100). An explicit, deliberately-scoped PATH
369
+ /// must never be refused regardless of its size. `Default`'s `false` is NOT a safe fallback for
370
+ /// the walk guard itself (it means "never refuse"); it only exists so ad hoc test fixtures and
371
+ /// the always-explicit-`--path` diagnostic commands (`gpu-native-stats`, cuda-graph benchmark)
372
+ /// get deterministic, non-refusing behavior, mirroring `NativeSearchConfig`'s convention.
373
+ pub path_was_implicit: bool,
364
374
  }
365
375
 
366
376
  #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
@@ -3784,6 +3794,47 @@ fn ensure_capacity(capacity: usize, required: usize, path: &Path) -> Result<()>
3784
3794
  ))
3785
3795
  }
3786
3796
 
3797
+ /// Bounded refuse-before-enumerate gate for the GPU-native engine's own root walk -- the
3798
+ /// cuda-native sibling of `native_search::check_native_implicit_walk_ceiling` (audit #105) and
3799
+ /// `rg_passthrough::check_implicit_walk_ceiling` (audit #100). Audit #109 found the GPU-native
3800
+ /// engine had NO ceiling at all: `GpuSearchParams::path_was_implicit` (main.rs) was threaded into
3801
+ /// the CPU-fallback `RipgrepSearchArgs`/`NativeSearchConfig` redirects (`execute_gpu_native_route`'s
3802
+ /// unavailable-GPU fallback path) but never into `GpuNativeSearchConfig` itself, so a bare
3803
+ /// implicit-path `tg search PAT --gpu-device-ids 0` on a huge root walked unbounded through this
3804
+ /// engine even though the CPU engine (#105) and rg-passthrough engine (#100) were both already
3805
+ /// bounded for the exact same shape of request.
3806
+ ///
3807
+ /// Only meaningful when `config.path_was_implicit` -- an explicit, deliberately-scoped PATH is
3808
+ /// never refused regardless of size. Called as the FIRST statement of `collect_walked_files`, the
3809
+ /// only function in this module that ever hands a root to `WalkBuilder`.
3810
+ fn check_gpu_native_implicit_walk_ceiling(
3811
+ config: &GpuNativeSearchConfig,
3812
+ roots: &[PathBuf],
3813
+ ) -> Option<String> {
3814
+ if !config.path_was_implicit {
3815
+ return None;
3816
+ }
3817
+ let probe_roots: Vec<String> = roots
3818
+ .iter()
3819
+ .map(|root| root.to_string_lossy().into_owned())
3820
+ .collect();
3821
+ if crate::rg_passthrough::implicit_search_walk_exceeds_ceiling(
3822
+ &probe_roots,
3823
+ config.max_depth,
3824
+ config.no_ignore,
3825
+ config.hidden,
3826
+ crate::rg_passthrough::IMPLICIT_SEARCH_WALK_FILE_CEILING,
3827
+ ) {
3828
+ Some(
3829
+ crate::rg_passthrough::format_unbounded_implicit_search_walk_error(
3830
+ crate::rg_passthrough::IMPLICIT_SEARCH_WALK_FILE_CEILING,
3831
+ ),
3832
+ )
3833
+ } else {
3834
+ None
3835
+ }
3836
+ }
3837
+
3787
3838
  fn collect_search_files(config: &GpuNativeSearchConfig) -> Result<Vec<PathBuf>> {
3788
3839
  let mut files = Vec::new();
3789
3840
  let mut roots = Vec::new();
@@ -3812,6 +3863,9 @@ fn collect_search_files(config: &GpuNativeSearchConfig) -> Result<Vec<PathBuf>>
3812
3863
  }
3813
3864
 
3814
3865
  fn collect_walked_files(config: &GpuNativeSearchConfig, roots: &[PathBuf]) -> Result<Vec<PathBuf>> {
3866
+ if let Some(refusal) = check_gpu_native_implicit_walk_ceiling(config, roots) {
3867
+ return Err(anyhow!(refusal));
3868
+ }
3815
3869
  let builder = build_walk_builder(config, roots)?;
3816
3870
  let walked_files = Arc::new(std::sync::Mutex::new(Vec::new()));
3817
3871
  let shared_files = Arc::clone(&walked_files);
@@ -4288,13 +4342,142 @@ fn ensure_cuda_library_path() {
4288
4342
  #[cfg(test)]
4289
4343
  mod tests {
4290
4344
  use super::{
4291
- cached_search_kernel_ptx_path, kernel_compile_options_for_compute_capability,
4292
- line_matches_for_file, load_file_batch_into_pinned_slice, resolve_adaptive_match_capacity,
4345
+ cached_search_kernel_ptx_path, check_gpu_native_implicit_walk_ceiling,
4346
+ collect_search_files, kernel_compile_options_for_compute_capability, line_matches_for_file,
4347
+ load_file_batch_into_pinned_slice, resolve_adaptive_match_capacity,
4293
4348
  resolve_max_match_capacity, validate_requested_cuda_device_ids, BatchedFile, FileBatchPlan,
4294
- LineDescriptor, PatternMatchPosition,
4349
+ GpuNativeSearchConfig, LineDescriptor, PatternMatchPosition,
4295
4350
  };
4296
4351
  use std::fs;
4297
- use std::path::PathBuf;
4352
+ use std::path::{Path, PathBuf};
4353
+
4354
+ // --- Audit #109: GPU-native implicit-walk-ceiling gate --------------------------------
4355
+ // Mirrors native_search.rs's audit #105 test suite for `check_native_implicit_walk_ceiling`.
4356
+ // #105 hoisted a walk-ceiling gate into the native-CPU engine but left the GPU-native engine
4357
+ // (this module) with NO ceiling at all -- `GpuNativeSearchConfig` did not even have a
4358
+ // `path_was_implicit` field, so a bare implicit-path `--gpu-device-ids` search on a huge root
4359
+ // walked unbounded through `collect_walked_files`.
4360
+
4361
+ fn make_stub_file_dir(dir: &Path, file_count: usize) {
4362
+ for index in 0..file_count {
4363
+ fs::write(
4364
+ dir.join(format!("stub_{index}.py")),
4365
+ "nothing interesting\n",
4366
+ )
4367
+ .unwrap();
4368
+ }
4369
+ }
4370
+
4371
+ fn config_with_paths(paths: Vec<PathBuf>, path_was_implicit: bool) -> GpuNativeSearchConfig {
4372
+ GpuNativeSearchConfig {
4373
+ patterns: vec!["TODO".to_string()],
4374
+ paths,
4375
+ path_was_implicit,
4376
+ ..GpuNativeSearchConfig::default()
4377
+ }
4378
+ }
4379
+
4380
+ #[test]
4381
+ fn check_gpu_native_implicit_walk_ceiling_refuses_oversized_implicit_walk() {
4382
+ // RED-before-fix: this is the exact shape of the #109 bypass -- an implicit-path search
4383
+ // (no explicit PATH positional) on a root over the 1500-file ceiling.
4384
+ let dir = tempfile::tempdir().unwrap();
4385
+ make_stub_file_dir(dir.path(), 1600);
4386
+ let roots = vec![dir.path().to_path_buf()];
4387
+ let config = config_with_paths(roots.clone(), true);
4388
+
4389
+ let refusal = check_gpu_native_implicit_walk_ceiling(&config, &roots);
4390
+
4391
+ assert!(
4392
+ refusal.is_some(),
4393
+ "an oversized implicit-path walk must be refused"
4394
+ );
4395
+ }
4396
+
4397
+ #[test]
4398
+ fn check_gpu_native_implicit_walk_ceiling_allows_explicit_path_even_when_oversized() {
4399
+ // Non-regression (Trap #3 parity, mirrors native_search.rs/rg_passthrough.rs): an
4400
+ // EXPLICIT, deliberately-scoped PATH must never be refused regardless of size.
4401
+ let dir = tempfile::tempdir().unwrap();
4402
+ make_stub_file_dir(dir.path(), 1600);
4403
+ let roots = vec![dir.path().to_path_buf()];
4404
+ let config = config_with_paths(roots.clone(), false);
4405
+
4406
+ let refusal = check_gpu_native_implicit_walk_ceiling(&config, &roots);
4407
+
4408
+ assert!(
4409
+ refusal.is_none(),
4410
+ "an explicit path must run uninhibited even when the walk exceeds the ceiling"
4411
+ );
4412
+ }
4413
+
4414
+ #[test]
4415
+ fn check_gpu_native_implicit_walk_ceiling_allows_implicit_path_under_ceiling() {
4416
+ // Normal-case non-regression: an implicit path under the ceiling is unaffected -- a
4417
+ // typical repo must never be refused.
4418
+ let dir = tempfile::tempdir().unwrap();
4419
+ make_stub_file_dir(dir.path(), 50);
4420
+ let roots = vec![dir.path().to_path_buf()];
4421
+ let config = config_with_paths(roots.clone(), true);
4422
+
4423
+ let refusal = check_gpu_native_implicit_walk_ceiling(&config, &roots);
4424
+
4425
+ assert!(
4426
+ refusal.is_none(),
4427
+ "a 50-file implicit root must not be refused"
4428
+ );
4429
+ }
4430
+
4431
+ #[test]
4432
+ fn collect_search_files_refuses_oversized_implicit_walk_before_enumerating() {
4433
+ // Hermetic end-to-end test of the actual `collect_search_files` production entry point
4434
+ // `gpu_native_search_paths_multi_with_options` calls before ever touching a CUDA device
4435
+ // (see that function: `collect_search_files(config)?` runs BEFORE `resolve_cuda_devices`),
4436
+ // so this test exercises the real bug with no GPU/CUDA runtime involved. Bounded per
4437
+ // anti-hang-test-protocol: run on a joined worker thread with an explicit timeout so a
4438
+ // regression (the gate silently stops firing) that falls through to the unbounded
4439
+ // parallel walk cannot hang the test runner -- it fails fast with a clear panic instead.
4440
+ let dir = tempfile::tempdir().unwrap();
4441
+ make_stub_file_dir(dir.path(), 1600);
4442
+ let config = config_with_paths(vec![dir.path().to_path_buf()], true);
4443
+
4444
+ let (tx, rx) = std::sync::mpsc::channel();
4445
+ std::thread::spawn(move || {
4446
+ let result = collect_search_files(&config).map_err(|error| error.to_string());
4447
+ let _ = tx.send(result);
4448
+ });
4449
+ let result = rx.recv_timeout(std::time::Duration::from_secs(10)).expect(
4450
+ "collect_search_files must return well within 10s -- a hang here means the \
4451
+ walk-ceiling gate did not fire before an unbounded parallel walk",
4452
+ );
4453
+
4454
+ let err = result.expect_err("an oversized implicit-path walk must be refused, not Ok");
4455
+ assert!(
4456
+ crate::rg_passthrough::is_unbounded_implicit_search_walk_refusal(&err),
4457
+ "unexpected error (expected the walk-ceiling refusal): {err}"
4458
+ );
4459
+ }
4460
+
4461
+ #[test]
4462
+ fn collect_search_files_does_not_refuse_explicit_oversized_path() {
4463
+ // Non-regression: an explicit PATH (even oversized) must complete normally, not be
4464
+ // refused -- fail-open for explicit scoping is the whole point of the guard (Trap #3
4465
+ // parity). Bounded per anti-hang-test-protocol.
4466
+ let dir = tempfile::tempdir().unwrap();
4467
+ make_stub_file_dir(dir.path(), 1600);
4468
+ let config = config_with_paths(vec![dir.path().to_path_buf()], false);
4469
+
4470
+ let (tx, rx) = std::sync::mpsc::channel();
4471
+ std::thread::spawn(move || {
4472
+ let result = collect_search_files(&config).map_err(|error| error.to_string());
4473
+ let _ = tx.send(result);
4474
+ });
4475
+ let result = rx
4476
+ .recv_timeout(std::time::Duration::from_secs(20))
4477
+ .expect("collect_search_files must return well within 20s for an explicit path");
4478
+
4479
+ result.expect("an explicit oversized path must not be refused");
4480
+ }
4298
4481
 
4299
4482
  #[test]
4300
4483
  fn validate_requested_cuda_device_ids_preserves_selected_subset_without_expanding() {
@@ -10663,6 +10663,10 @@ fn gpu_native_config_from_internal_args(args: &GpuNativeStatsArgs) -> GpuNativeS
10663
10663
  hidden: false,
10664
10664
  max_depth: None,
10665
10665
  max_batch_bytes: args.max_batch_bytes,
10666
+ // `GpuNativeStatsArgs::path` is a required `#[arg(long)]` (no default_value) -- this
10667
+ // diagnostic command always has an explicit, deliberately-scoped PATH, never an implicit
10668
+ // one, so the #109 ceiling must never fire here regardless of tree size.
10669
+ path_was_implicit: false,
10666
10670
  }
10667
10671
  }
10668
10672
 
@@ -10676,6 +10680,10 @@ fn gpu_native_config_from_graph_args(args: &GpuCudaGraphArgs) -> GpuNativeSearch
10676
10680
  hidden: false,
10677
10681
  max_depth: None,
10678
10682
  max_batch_bytes: args.max_batch_bytes,
10683
+ // `GpuCudaGraphArgs::path` is a required `#[arg(long)]` (no default_value) -- this
10684
+ // benchmark command always has an explicit, deliberately-scoped PATH, never an implicit
10685
+ // one, so the #109 ceiling must never fire here regardless of tree size.
10686
+ path_was_implicit: false,
10679
10687
  }
10680
10688
  }
10681
10689
 
@@ -10764,6 +10772,12 @@ fn execute_gpu_native_route(
10764
10772
  hidden: params.hidden,
10765
10773
  max_depth: params.max_depth,
10766
10774
  max_batch_bytes: None,
10775
+ // Audit #109 fix: this field did not exist on `GpuNativeSearchConfig` at all, so the
10776
+ // GPU-native engine had no equivalent of the #105 native-CPU implicit-walk ceiling --
10777
+ // `params.path_was_implicit` was already threaded into the CPU-fallback redirects
10778
+ // (`ripgrep_args_for_gpu_params`, `native_search_config_for_gpu_params`) but silently
10779
+ // dropped here, the one construction site that feeds the actual cuda kernel walk.
10780
+ path_was_implicit: params.path_was_implicit,
10767
10781
  };
10768
10782
 
10769
10783
  let stats = gpu_native_search_paths_multi(&config, device_ids)?;
@@ -46,6 +46,7 @@ fn test_gpu_native_replays_captured_cuda_graphs_without_changing_results() {
46
46
  hidden: false,
47
47
  max_depth: None,
48
48
  max_batch_bytes: Some(4 * 1024),
49
+ path_was_implicit: false,
49
50
  };
50
51
 
51
52
  let benchmark = benchmark_cuda_graph_search_paths(&config, device_id).unwrap();
@@ -83,6 +84,7 @@ fn test_gpu_native_cuda_graphs_reduce_batch_overhead_by_ten_percent() {
83
84
  hidden: false,
84
85
  max_depth: None,
85
86
  max_batch_bytes: Some(4 * 1024),
87
+ path_was_implicit: false,
86
88
  };
87
89
 
88
90
  let benchmark = benchmark_cuda_graph_search_paths(&config, device_id).unwrap();
@@ -777,6 +777,7 @@ fn test_gpu_native_multi_pattern_falls_back_to_batched_passes_for_large_pattern_
777
777
  hidden: false,
778
778
  max_depth: None,
779
779
  max_batch_bytes: Some(8 * 1024),
780
+ path_was_implicit: false,
780
781
  };
781
782
 
782
783
  let stats = gpu_native_search_paths(&config, device_id).unwrap();
@@ -821,6 +822,7 @@ fn test_gpu_native_three_pattern_dispatch_is_below_two_times_single_pattern() {
821
822
  hidden: false,
822
823
  max_depth: None,
823
824
  max_batch_bytes: None,
825
+ path_was_implicit: false,
824
826
  };
825
827
  let multi_config = GpuNativeSearchConfig {
826
828
  patterns: vec![
@@ -834,6 +836,7 @@ fn test_gpu_native_three_pattern_dispatch_is_below_two_times_single_pattern() {
834
836
  hidden: false,
835
837
  max_depth: None,
836
838
  max_batch_bytes: None,
839
+ path_was_implicit: false,
837
840
  };
838
841
 
839
842
  let mut single_samples = Vec::new();
@@ -119,6 +119,7 @@ fn test_gpu_native_adaptive_dispatch_matches_cpu_for_ten_kib_lines() {
119
119
  hidden: false,
120
120
  max_depth: None,
121
121
  max_batch_bytes: Some(256 * 1024),
122
+ path_was_implicit: false,
122
123
  };
123
124
 
124
125
  let stats = gpu_native_search_paths(&config, device_id).unwrap();
@@ -161,6 +162,7 @@ fn test_gpu_native_adaptive_dispatch_matches_cpu_for_hundred_kib_lines() {
161
162
  hidden: false,
162
163
  max_depth: None,
163
164
  max_batch_bytes: Some(512 * 1024),
165
+ path_was_implicit: false,
164
166
  };
165
167
 
166
168
  let stats = gpu_native_search_paths(&config, device_id).unwrap();
@@ -202,6 +204,7 @@ fn test_gpu_native_adaptive_dispatch_classifies_mixed_short_medium_and_long_line
202
204
  hidden: false,
203
205
  max_depth: None,
204
206
  max_batch_bytes: Some(256 * 1024),
207
+ path_was_implicit: false,
205
208
  };
206
209
 
207
210
  let stats = gpu_native_search_paths(&config, device_id).unwrap();
@@ -263,6 +266,7 @@ fn test_gpu_native_long_line_corpus_is_not_slower_than_short_line_corpus() {
263
266
  hidden: false,
264
267
  max_depth: None,
265
268
  max_batch_bytes: Some(2 * 1024 * 1024),
269
+ path_was_implicit: false,
266
270
  };
267
271
  let long_config = GpuNativeSearchConfig {
268
272
  patterns: vec![pattern.to_string()],
@@ -272,6 +276,7 @@ fn test_gpu_native_long_line_corpus_is_not_slower_than_short_line_corpus() {
272
276
  hidden: false,
273
277
  max_depth: None,
274
278
  max_batch_bytes: Some(2 * 1024 * 1024),
279
+ path_was_implicit: false,
275
280
  };
276
281
 
277
282
  let mut short_samples = Vec::new();
@@ -80,6 +80,7 @@ fn test_gpu_native_search_reports_pinned_double_buffered_stream_pipeline() {
80
80
  hidden: false,
81
81
  max_depth: None,
82
82
  max_batch_bytes: Some(64 * 1024),
83
+ path_was_implicit: false,
83
84
  };
84
85
 
85
86
  let stats = gpu_native_search_paths(&config, device_id).unwrap();
@@ -107,6 +108,7 @@ fn test_gpu_native_overlap_matches_expected_results_without_races() {
107
108
  hidden: false,
108
109
  max_depth: None,
109
110
  max_batch_bytes: Some(64 * 1024),
111
+ path_was_implicit: false,
110
112
  };
111
113
 
112
114
  let mut actual = gpu_native_search_paths(&config, device_id)
@@ -138,6 +140,7 @@ fn test_gpu_native_overlap_metrics_show_transfer_compute_overlap() {
138
140
  hidden: false,
139
141
  max_depth: None,
140
142
  max_batch_bytes: Some(64 * 1024),
143
+ path_was_implicit: false,
141
144
  };
142
145
 
143
146
  let stats = gpu_native_search_paths(&config, device_id).unwrap();
@@ -189,6 +192,7 @@ fn test_gpu_native_multi_gpu_balances_distribution_and_matches_single_gpu_result
189
192
  hidden: false,
190
193
  max_depth: None,
191
194
  max_batch_bytes: Some(128 * 1024),
195
+ path_was_implicit: false,
192
196
  };
193
197
 
194
198
  let single = gpu_native_search_paths(&config, devices[0].device_id).unwrap();
@@ -372,14 +372,71 @@ def _release_daemon_start_lock(root: Path) -> None:
372
372
  pass
373
373
 
374
374
 
375
- def _remove_daemon_metadata(root: Path) -> None:
375
+ def _remove_daemon_metadata(
376
+ root: Path, *, expected_pid: int | None = None, expected_port: int | None = None
377
+ ) -> None:
378
+ """Remove ``daemon.json`` for ``root``, guarded against deleting a REPLACEMENT daemon's metadata.
379
+
380
+ Task #143a-a (stale-daemon-metadata race): when ``_probe_daemon`` rejects a daemon over a
381
+ ``package_version`` mismatch (see the skew check in ``_probe_daemon`` below), the client spawns
382
+ a replacement daemon (call it B) while the old daemon (A) keeps running until its own
383
+ idle/max-uptime self-shutdown fires. If A's shutdown path (or ``stop_session_daemon``'s
384
+ PID-kill fallback) unlinked ``daemon.json`` unconditionally, it would delete WHATEVER metadata
385
+ currently exists -- including B's, the healthy replacement -- orphaning B (it keeps serving,
386
+ but no client can discover it) and silently piling up orphaned daemon processes over time.
387
+
388
+ Passing ``expected_pid``/``expected_port`` re-reads the CURRENT on-disk metadata immediately
389
+ before unlinking and only proceeds if its ``pid``/``port`` fields still match -- i.e. the file
390
+ still identifies the exact instance the caller intends to remove. A mismatch (a replacement has
391
+ since published its own metadata), a missing file, or an unparseable/corrupt file are all
392
+ treated as "not mine" and left untouched -- the fail-safe choice, since we cannot prove the file
393
+ is safe to delete, and a corrupt file self-heals the next time a daemon is (re)started via
394
+ ``_spawn_daemon_subprocess``'s unconditional clear below.
395
+
396
+ When BOTH ``expected_pid`` and ``expected_port`` are omitted (the default), removal is
397
+ unconditional -- this mode exists for exactly one legitimate caller, ``_spawn_daemon_subprocess``,
398
+ which holds the daemon-start lock (excluding any concurrent spawn for this root) and is clearing
399
+ the slate for a brand-new instance it is about to launch, so there is no prior "self" identity to
400
+ compare against yet. Every other caller must supply at least one ``expected_*`` value, or skip
401
+ the call entirely when it cannot establish one (see ``stop_session_daemon``).
402
+ """
376
403
  metadata_path = _daemon_metadata_path(root)
404
+ if expected_pid is not None or expected_port is not None:
405
+ current = _read_daemon_metadata(root)
406
+ if current is None:
407
+ return
408
+ if expected_pid is not None and current.get("pid") != expected_pid:
409
+ return
410
+ if expected_port is not None and current.get("port") != expected_port:
411
+ return
377
412
  try:
378
413
  metadata_path.unlink()
379
414
  except OSError:
380
415
  pass
381
416
 
382
417
 
418
+ def _daemon_identity(metadata: dict[str, Any] | None) -> tuple[int | None, int | None]:
419
+ """Best-effort ``(pid, port)`` extraction from daemon metadata (task #143a-a).
420
+
421
+ Used by callers of the guarded ``_remove_daemon_metadata`` above to compute the specific
422
+ instance they intend to remove. Missing or non-numeric fields resolve to ``None`` rather than
423
+ raising, so a malformed or partial metadata dict never turns into a removal call the guard
424
+ cannot safely reason about -- the caller is expected to skip the removal when either half of
425
+ the pair it needs comes back ``None`` (see ``stop_session_daemon``).
426
+ """
427
+ if not metadata:
428
+ return None, None
429
+ try:
430
+ pid: int | None = int(metadata["pid"])
431
+ except (KeyError, TypeError, ValueError):
432
+ pid = None
433
+ try:
434
+ port: int | None = int(metadata["port"])
435
+ except (KeyError, TypeError, ValueError):
436
+ port = None
437
+ return pid, port
438
+
439
+
383
440
  def _pid_looks_like_tg_daemon(pid: int) -> bool:
384
441
  """Best-effort check that ``pid`` is a tensor-grep session daemon (audit I7).
385
442
 
@@ -630,6 +687,12 @@ def _spawn_daemon_subprocess(root: Path) -> None:
630
687
  ``daemon.json`` to appear and does not touch the start-lock -- the caller is responsible for
631
688
  both (holding the lock across this call, and deciding whether/how long to wait afterward).
632
689
  """
690
+ # Task #143a-a: unconditional (force) removal is intentional and safe here -- every caller
691
+ # holds the daemon-start lock for `root` (excluding any concurrent spawn), so nothing else can
692
+ # be publishing fresh metadata for this root while we hold it, and we are about to Popen a
693
+ # brand-new instance that will publish its OWN metadata momentarily. There is no prior "self"
694
+ # identity to compare against yet, unlike the guarded callers in stop_session_daemon and
695
+ # run_session_daemon_server's shutdown path below.
633
696
  _remove_daemon_metadata(root)
634
697
  creationflags = 0
635
698
  env = os.environ.copy()
@@ -772,7 +835,16 @@ def stop_session_daemon(path: str = ".") -> dict[str, Any]:
772
835
  # socket is wedged). Fall back to terminating the recorded pid if it validates.
773
836
  stale_metadata = _read_daemon_metadata(root)
774
837
  killed = _terminate_daemon_by_pid(stale_metadata)
775
- _remove_daemon_metadata(root)
838
+ # Task #143a-a: only remove the metadata that still identifies the STALE daemon we just
839
+ # targeted -- never whatever happens to be on disk by the time we get here. A concurrent
840
+ # autostart elsewhere may have already spawned and published a healthy replacement's
841
+ # daemon.json in the window between the failed probe above and this cleanup; an
842
+ # unconditional unlink would delete that replacement's metadata too. When the stale
843
+ # metadata itself cannot be identified (missing/corrupt), there is nothing safe to
844
+ # remove -- skip rather than guess.
845
+ stale_pid, stale_port = _daemon_identity(stale_metadata)
846
+ if stale_pid is not None:
847
+ _remove_daemon_metadata(root, expected_pid=stale_pid, expected_port=stale_port)
776
848
  return {
777
849
  "version": _SESSION_VERSION,
778
850
  "root": str(root),
@@ -802,7 +874,12 @@ def stop_session_daemon(path: str = ".") -> dict[str, Any]:
802
874
  # validated pid terminate so a wedged daemon is not left running.
803
875
  if _terminate_daemon_by_pid(metadata):
804
876
  stop_method = "pid"
805
- _remove_daemon_metadata(root)
877
+ # Task #143a-a: only remove daemon.json if it still identifies the SAME daemon this call
878
+ # targeted (captured in `metadata` above) -- a replacement may have spawned and published its
879
+ # own metadata in the window since. See _remove_daemon_metadata's docstring for the full race.
880
+ target_pid, target_port = _daemon_identity(metadata)
881
+ if target_pid is not None:
882
+ _remove_daemon_metadata(root, expected_pid=target_pid, expected_port=target_port)
806
883
  response["running"] = False
807
884
  response["root"] = str(root)
808
885
  response["stopped"] = True
@@ -1965,7 +2042,13 @@ def run_session_daemon_server(path: str = ".") -> None:
1965
2042
  finally:
1966
2043
  stop_event.set()
1967
2044
  _flush_demand_metrics_if_dirty(server, force=True)
1968
- _remove_daemon_metadata(root)
2045
+ # Task #143a-a: this daemon must only ever remove ITS OWN daemon.json on shutdown
2046
+ # (idle timeout, max-uptime, or a cooperative "stop" command). If a client already
2047
+ # rejected this daemon over a package_version mismatch and spawned a healthy
2048
+ # replacement, that replacement's metadata is what is on disk by the time THIS daemon
2049
+ # finally shuts itself down -- an unconditional unlink here would delete it out from
2050
+ # under the replacement, orphaning it with no discoverable daemon.json.
2051
+ _remove_daemon_metadata(root, expected_pid=os.getpid(), expected_port=int(port))
1969
2052
 
1970
2053
 
1971
2054
  def _parse_args(argv: list[str]) -> argparse.Namespace:
File without changes
File without changes