tensor-grep 1.76.0__tar.gz → 1.76.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.
Files changed (135) hide show
  1. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/PKG-INFO +1 -1
  2. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/pyproject.toml +3 -1
  3. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/Cargo.lock +1 -1
  4. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/Cargo.toml +3 -1
  5. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/main.rs +124 -3
  6. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/checkpoint_store.py +29 -24
  7. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/repo_map.py +25 -6
  8. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/runtime_paths.py +7 -0
  9. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/LICENSE +0 -0
  10. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/NOTICE +0 -0
  11. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/README.md +0 -0
  12. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/deny.toml +0 -0
  13. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/rust-toolchain.toml +0 -0
  14. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/backend_ast.rs +0 -0
  15. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/backend_ast_workflow.rs +0 -0
  16. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/backend_cpu.rs +0 -0
  17. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/bin/tg_search_fast.rs +0 -0
  18. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/cli.rs +0 -0
  19. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/crossover.rs +0 -0
  20. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/editor_plane.rs +0 -0
  21. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/gpu_native.rs +0 -0
  22. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/index.rs +0 -0
  23. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/index_lock.rs +0 -0
  24. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/lib.rs +0 -0
  25. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/mmap_arrow.rs +0 -0
  26. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/native_search.rs +0 -0
  27. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/python_sidecar.rs +0 -0
  28. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/rg_passthrough.rs +0 -0
  29. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/routing.rs +0 -0
  30. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/src/runtime_paths.rs +0 -0
  31. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_ast_backend.rs +0 -0
  32. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_ast_cache.rs +0 -0
  33. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_ast_grep_embed.rs +0 -0
  34. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_ast_rewrite.rs +0 -0
  35. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_audit_manifest_symlink_unix.rs +0 -0
  36. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_gpu_native.rs +0 -0
  37. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_gpu_native_error_handling.rs +0 -0
  38. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_gpu_native_graphs.rs +0 -0
  39. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_gpu_native_integration.rs +0 -0
  40. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_gpu_native_long_lines.rs +0 -0
  41. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_gpu_native_streams.rs +0 -0
  42. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_index.rs +0 -0
  43. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_index_lock_concurrency.rs +0 -0
  44. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_mmap_arrow.rs +0 -0
  45. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_native_search.rs +0 -0
  46. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_output.rs +0 -0
  47. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_public_native_cli_parity.rs +0 -0
  48. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_python_bootstrap.rs +0 -0
  49. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_release_profile.rs +0 -0
  50. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_replace.rs +0 -0
  51. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_resident_worker.rs +0 -0
  52. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_routing.rs +0 -0
  53. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_runtime_path_resolution.rs +0 -0
  54. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_schema_compat.rs +0 -0
  55. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_search.rs +0 -0
  56. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_search_golden.rs +0 -0
  57. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_sidecar_ipc.rs +0 -0
  58. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/rust_core/tests/test_smart_routing.rs +0 -0
  59. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/__init__.py +0 -0
  60. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/__main__.py +0 -0
  61. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/__init__.py +0 -0
  62. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/ast_backend.py +0 -0
  63. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/ast_wrapper_backend.py +0 -0
  64. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/base.py +0 -0
  65. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/cpu_backend.py +0 -0
  66. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/cudf_backend.py +0 -0
  67. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/cybert_backend.py +0 -0
  68. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/ripgrep_backend.py +0 -0
  69. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/rust_backend.py +0 -0
  70. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/stringzilla_backend.py +0 -0
  71. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/backends/torch_backend.py +0 -0
  72. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/__init__.py +0 -0
  73. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/_index_lock.py +0 -0
  74. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/agent_capsule.py +0 -0
  75. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/apply_policy.py +0 -0
  76. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/ast_workflows.py +0 -0
  77. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/audit_manifest.py +0 -0
  78. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/bootstrap.py +0 -0
  79. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/codemap.py +0 -0
  80. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/commands.py +0 -0
  81. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/docs_coverage.py +0 -0
  82. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/dogfood.py +0 -0
  83. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/evidence_receipt.py +0 -0
  84. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/evidence_signing.py +0 -0
  85. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/formatters/__init__.py +0 -0
  86. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/formatters/base.py +0 -0
  87. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/formatters/csv_fmt.py +0 -0
  88. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/formatters/json_fmt.py +0 -0
  89. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/formatters/ripgrep_fmt.py +0 -0
  90. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/formatters/table_fmt.py +0 -0
  91. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/inventory.py +0 -0
  92. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/lang_go.py +0 -0
  93. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/lang_registry.py +0 -0
  94. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/lsp_external_provider.py +0 -0
  95. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/lsp_provider_setup.py +0 -0
  96. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/lsp_server.py +0 -0
  97. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/main.py +0 -0
  98. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/mcp_server.py +0 -0
  99. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/orient_capsule.py +0 -0
  100. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/progress.py +0 -0
  101. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/rg_contract.py +0 -0
  102. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/rule_packs.py +0 -0
  103. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/scan_guardrails.py +0 -0
  104. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/session_daemon.py +0 -0
  105. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/session_store.py +0 -0
  106. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/cli/subprocess_policy.py +0 -0
  107. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/__init__.py +0 -0
  108. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/config.py +0 -0
  109. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/hardware/__init__.py +0 -0
  110. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/hardware/device_detect.py +0 -0
  111. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/hardware/device_inventory.py +0 -0
  112. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/hardware/memory_manager.py +0 -0
  113. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/observability.py +0 -0
  114. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/pipeline.py +0 -0
  115. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/query_analyzer.py +0 -0
  116. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/registration_check.py +0 -0
  117. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/reranker.py +0 -0
  118. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/result.py +0 -0
  119. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/retrieval_bm25.py +0 -0
  120. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/retrieval_chunker.py +0 -0
  121. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/retrieval_dense.py +0 -0
  122. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/retrieval_fusion.py +0 -0
  123. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/retrieval_late.py +0 -0
  124. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/retrieval_lexical.py +0 -0
  125. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/retrieval_scoring.py +0 -0
  126. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/core/semantic_index.py +0 -0
  127. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/io/__init__.py +0 -0
  128. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/io/base.py +0 -0
  129. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/io/directory_scanner.py +0 -0
  130. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/io/reader_cudf.py +0 -0
  131. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/io/reader_dstorage.py +0 -0
  132. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/io/reader_fallback.py +0 -0
  133. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/io/reader_kvikio.py +0 -0
  134. {tensor_grep-1.76.0 → tensor_grep-1.76.2}/src/tensor_grep/perf_guard.py +0 -0
  135. {tensor_grep-1.76.0 → tensor_grep-1.76.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.76.0
3
+ Version: 1.76.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
@@ -528,12 +528,14 @@ version_variables = [
528
528
 
529
529
 
530
530
 
531
+
532
+
531
533
 
532
534
 
533
535
 
534
536
  [project]
535
537
  name = "tensor-grep"
536
- version = "1.76.0"
538
+ version = "1.76.2"
537
539
  description = "High-performance grep-compatible CLI with Rust, GPU, and AST/NLP routing"
538
540
  readme = { file = "rust_core/README.md", content-type = "text/markdown" }
539
541
  requires-python = ">=3.11"
@@ -1579,7 +1579,7 @@ dependencies = [
1579
1579
 
1580
1580
  [[package]]
1581
1581
  name = "tensor_grep_rs"
1582
- version = "1.76.0"
1582
+ version = "1.76.2"
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.0"
3
+ version = "1.76.2"
4
4
  edition = "2021"
5
5
  description = "Rust core engine for tensor-grep"
6
6
  license = "Apache-2.0"
@@ -436,6 +436,8 @@ path = "src/bin/tg_search_fast.rs"
436
436
 
437
437
 
438
438
 
439
+
440
+
439
441
 
440
442
 
441
443
 
@@ -5049,6 +5049,122 @@ mod tests {
5049
5049
  .is_empty()
5050
5050
  );
5051
5051
  }
5052
+
5053
+ /// Creates a symlink at `link` pointing to `target`, dispatching to the platform-specific
5054
+ /// primitive (mirrors write_bytes_refuse_symlink's own unix/windows/other split). Tests
5055
+ /// call this and skip gracefully (matching the existing audit-manifest symlink tests) when
5056
+ /// the sandbox lacks symlink privilege instead of failing the whole suite.
5057
+ fn try_symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
5058
+ #[cfg(unix)]
5059
+ {
5060
+ std::os::unix::fs::symlink(target, link)
5061
+ }
5062
+ #[cfg(windows)]
5063
+ {
5064
+ std::os::windows::fs::symlink_file(target, link)
5065
+ }
5066
+ #[cfg(not(any(unix, windows)))]
5067
+ {
5068
+ let _ = (target, link);
5069
+ Err(std::io::Error::new(
5070
+ std::io::ErrorKind::Unsupported,
5071
+ "symlinks not supported on this platform",
5072
+ ))
5073
+ }
5074
+ }
5075
+
5076
+ #[test]
5077
+ fn test_create_checkpoint_refuses_symlink_at_index_path() {
5078
+ // #115 Gap 2: checkpoint_index_path (checkpoint_storage_dir(root).join("index.json"))
5079
+ // is a SHARED, PREDICTABLE path across every checkpoint under a given root -- unlike
5080
+ // metadata.json (namespaced under a random checkpoint_id, unpredictable ahead of the
5081
+ // call), an attacker who can write into the checkpoint root can plant a symlink at
5082
+ // index.json in advance of any create_checkpoint call. Confirm create_checkpoint
5083
+ // refuses to follow it instead of silently overwriting whatever the symlink targets.
5084
+ let dir = tempfile::tempdir().unwrap();
5085
+ let file_path = dir.path().join("fixture.py");
5086
+ std::fs::write(&file_path, "def add(x, y): return x + y\n").unwrap();
5087
+
5088
+ let outside_dir = tempfile::tempdir().unwrap();
5089
+ let outside_target = outside_dir.path().join("outside-target.json");
5090
+ // Valid (empty-array) JSON so the pre-write "does index.json already exist -> read
5091
+ // + parse it" branch succeeds and execution actually reaches the write call this
5092
+ // test targets, instead of failing earlier on a JSON-parse error.
5093
+ std::fs::write(&outside_target, b"[]").unwrap();
5094
+
5095
+ // Reuse the production scope-detection so the storage dir we plant the symlink under
5096
+ // is byte-identical to the one create_checkpoint computes internally (both must agree
5097
+ // post-canonicalization, or the symlink would land at the wrong path).
5098
+ let path_str = file_path.to_str().unwrap();
5099
+ let scope = detect_checkpoint_scope(path_str);
5100
+ let storage_dir = checkpoint_storage_dir(&scope.root);
5101
+ std::fs::create_dir_all(&storage_dir).unwrap();
5102
+ let index_path = storage_dir.join("index.json");
5103
+ if let Err(err) = try_symlink_file(&outside_target, &index_path) {
5104
+ eprintln!(
5105
+ "skipping test_create_checkpoint_refuses_symlink_at_index_path: cannot create a symlink in this environment: {err}"
5106
+ );
5107
+ return;
5108
+ }
5109
+
5110
+ let result = create_checkpoint(path_str);
5111
+ assert!(
5112
+ result.is_err(),
5113
+ "create_checkpoint must refuse to write the shared index through a symlink"
5114
+ );
5115
+ assert_eq!(
5116
+ std::fs::read(&outside_target).unwrap(),
5117
+ b"[]",
5118
+ "the symlink's target outside the checkpoint root must be left untouched"
5119
+ );
5120
+ }
5121
+
5122
+ #[test]
5123
+ fn test_restore_validation_rollback_snapshots_refuses_symlink_at_file_path() {
5124
+ // #115 Gap 3: between "apply the edit" and a failed-validation rollback, an edited
5125
+ // file could be swapped for a symlink pointing outside the project (e.g. via a
5126
+ // hostile --lint-cmd/--test-cmd running between apply and rollback).
5127
+ // restore_validation_rollback_snapshots must refuse to follow it instead of writing
5128
+ // the pre-edit snapshot bytes through the symlink.
5129
+ let dir = tempfile::tempdir().unwrap();
5130
+ let file_path = dir.path().join("fixture.py");
5131
+ let original_bytes = b"def add(x, y): return x + y\n".to_vec();
5132
+ std::fs::write(&file_path, &original_bytes).unwrap();
5133
+
5134
+ let outside_dir = tempfile::tempdir().unwrap();
5135
+ let outside_target = outside_dir.path().join("outside-target.py");
5136
+ std::fs::write(&outside_target, b"UNTOUCHED").unwrap();
5137
+
5138
+ let mut snapshots = BTreeMap::new();
5139
+ snapshots.insert(file_path.to_string_lossy().to_string(), original_bytes);
5140
+
5141
+ // Simulate the swap: the tracked file is replaced by a symlink after the pre-apply
5142
+ // snapshot was captured.
5143
+ std::fs::remove_file(&file_path).unwrap();
5144
+ if let Err(err) = try_symlink_file(&outside_target, &file_path) {
5145
+ eprintln!(
5146
+ "skipping test_restore_validation_rollback_snapshots_refuses_symlink_at_file_path: cannot create a symlink in this environment: {err}"
5147
+ );
5148
+ return;
5149
+ }
5150
+
5151
+ let summary = restore_validation_rollback_snapshots(&snapshots);
5152
+
5153
+ assert!(
5154
+ !summary.success,
5155
+ "restore must report failure when a snapshot target is a symlink"
5156
+ );
5157
+ assert!(
5158
+ summary.files_restored.is_empty(),
5159
+ "a refused symlink write must not be counted as restored"
5160
+ );
5161
+ assert_eq!(summary.errors.len(), 1);
5162
+ assert_eq!(
5163
+ std::fs::read(&outside_target).unwrap(),
5164
+ b"UNTOUCHED",
5165
+ "the symlink's target outside the rollback root must be left untouched"
5166
+ );
5167
+ }
5052
5168
  }
5053
5169
 
5054
5170
  fn run_command_cli(cli: CommandCli) -> anyhow::Result<()> {
@@ -8324,7 +8440,7 @@ fn create_checkpoint(path: &str) -> anyhow::Result<CheckpointCreateSummary> {
8324
8440
  std::fs::create_dir_all(parent)
8325
8441
  .with_context(|| format!("failed to create {}", parent.display()))?;
8326
8442
  }
8327
- std::fs::write(&metadata_path, serde_json::to_vec_pretty(&metadata)?)
8443
+ write_bytes_refuse_symlink(&metadata_path, &serde_json::to_vec_pretty(&metadata)?)
8328
8444
  .with_context(|| format!("failed to write {}", metadata_path.display()))?;
8329
8445
 
8330
8446
  let index_path = checkpoint_index_path(&root);
@@ -8352,7 +8468,7 @@ fn create_checkpoint(path: &str) -> anyhow::Result<CheckpointCreateSummary> {
8352
8468
  std::fs::create_dir_all(parent)
8353
8469
  .with_context(|| format!("failed to create {}", parent.display()))?;
8354
8470
  }
8355
- std::fs::write(&index_path, serde_json::to_vec_pretty(&records)?)
8471
+ write_bytes_refuse_symlink(&index_path, &serde_json::to_vec_pretty(&records)?)
8356
8472
  .with_context(|| format!("failed to write {}", index_path.display()))?;
8357
8473
 
8358
8474
  Ok(summary)
@@ -8972,7 +9088,7 @@ fn restore_validation_rollback_snapshots(
8972
9088
  let mut errors = Vec::new();
8973
9089
 
8974
9090
  for (file, bytes) in snapshots {
8975
- match std::fs::write(file, bytes) {
9091
+ match write_bytes_refuse_symlink(Path::new(file), bytes) {
8976
9092
  Ok(()) => files_restored.push(file.clone()),
8977
9093
  Err(error) => errors.push(format!("failed to restore {file}: {error}")),
8978
9094
  }
@@ -9007,6 +9123,11 @@ fn emit_rollback_status(summary: &ValidationRollbackSummary) {
9007
9123
  /// previously followed by a plain `std::fs::write` -- a confined write could escape its
9008
9124
  /// anchor. Mirrors the confine-then-open discipline `_write_json_refuse_symlink` already
9009
9125
  /// uses on the Python side (`src/tensor_grep/cli/main.py`).
9126
+ ///
9127
+ /// audit #115: also guards `create_checkpoint`'s metadata.json + the shared/predictable
9128
+ /// index.json write, and `restore_validation_rollback_snapshots`'s per-file restore write --
9129
+ /// the same TOCTOU class, just with the checkpoint/rollback root standing in for
9130
+ /// `--audit-manifest`'s confined target.
9010
9131
  fn write_bytes_refuse_symlink(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {
9011
9132
  use std::fs::OpenOptions;
9012
9133
 
@@ -853,33 +853,38 @@ def create_checkpoint(path: str = ".") -> CheckpointCreateResult:
853
853
  # follow_symlinks=False: store a symlink AS a link, never copy its (possibly
854
854
  # out-of-root) target content into the snapshot (audit HIGH — symlink disclosure).
855
855
  shutil.copy2(source, destination, follow_symlinks=False)
856
- except Exception:
857
- # audit H4: never leave a half-copied checkpoint dir behind on any abort, budget-
858
- # related or not (e.g. disk fills mid-copy despite the pre-flight, or a file
859
- # vanishes/grows mid-loop). Remove the WHOLE per-checkpoint dir (metadata.json has not
860
- # been written yet at this point, but snapshot_dir's own parent must go too, not just
861
- # the snapshot/ subdir) -- matches how _prune_checkpoint_records removes a checkpoint.
856
+
857
+ result = CheckpointCreateResult(
858
+ checkpoint_id=checkpoint_id,
859
+ mode=mode,
860
+ root=str(root),
861
+ created_at=created_at,
862
+ file_count=len(entries),
863
+ undo_argv=_undo_argv(scope, checkpoint_id),
864
+ undo_command=_display_command(_undo_argv(scope, checkpoint_id)),
865
+ skipped_nested_repos=skipped_nested_repos,
866
+ )
867
+ _write_checkpoint_metadata(
868
+ root,
869
+ result,
870
+ entries,
871
+ scope_kind=scope.scope_kind,
872
+ original_path=scope.original_path,
873
+ )
874
+ except BaseException:
875
+ # audit #125a: catch BaseException, not Exception -- KeyboardInterrupt/SystemExit
876
+ # subclass BaseException directly (not Exception), so a Ctrl+C here previously escaped
877
+ # this handler entirely and left an uncleaned checkpoint dir behind. The guarded region
878
+ # also now extends through the metadata write below (not just the copy loop above): a
879
+ # failure writing metadata.json after a fully successful copy must not orphan that
880
+ # directory either -- audit H4's original cleanup only covered the copy loop. Remove the
881
+ # WHOLE per-checkpoint dir (metadata.json may or may not have been written yet, but
882
+ # snapshot_dir's own parent must go too, not just the snapshot/ subdir) -- matches how
883
+ # _prune_checkpoint_records removes a checkpoint. Always re-raise: the interrupt or
884
+ # exception must still propagate to the caller, never be swallowed here.
862
885
  shutil.rmtree(snapshot_dir.parent, ignore_errors=True)
863
886
  raise
864
887
 
865
- result = CheckpointCreateResult(
866
- checkpoint_id=checkpoint_id,
867
- mode=mode,
868
- root=str(root),
869
- created_at=created_at,
870
- file_count=len(entries),
871
- undo_argv=_undo_argv(scope, checkpoint_id),
872
- undo_command=_display_command(_undo_argv(scope, checkpoint_id)),
873
- skipped_nested_repos=skipped_nested_repos,
874
- )
875
- _write_checkpoint_metadata(
876
- root,
877
- result,
878
- entries,
879
- scope_kind=scope.scope_kind,
880
- original_path=scope.original_path,
881
- )
882
-
883
888
  # q10 RMW race: load->mutate->write must be atomic w.r.t. every other writer of this
884
889
  # index.json, cross-process and cross-thread, or a concurrent insert can be lost (see
885
890
  # _index_lock.index_lock docstring / AGENTS.md Backend Fail-Closed Contract). Retention
@@ -6412,12 +6412,20 @@ def _python_module_matches_definition(
6412
6412
  repo_root: Path | str | None = None,
6413
6413
  *,
6414
6414
  level: int = 0,
6415
- ) -> bool:
6416
- return bool(
6417
- _python_module_match_details(
6418
- importer_path, module_name, definition_path, repo_root, level=level
6419
- )["matched"]
6415
+ ) -> tuple[bool, list[str]]:
6416
+ """Return `(matched, provenance)`.
6417
+
6418
+ Unlike the bool-only `_js_ts_module_matches_definition` / `_rust_module_matches_definition`
6419
+ siblings, this also threads through `_python_module_match_details`'s `provenance` (notably
6420
+ the "sys-path-insert" tag) -- #155 fix: that tag was computed but provably unreachable
6421
+ (this was the only caller, and it discarded everything but the bool) before this change.
6422
+ The sole caller, `_confirm_import_edges`, uses it to report the tag honestly on `tg
6423
+ importers` reverse edges instead of silently collapsing it into a generic label.
6424
+ """
6425
+ details = _python_module_match_details(
6426
+ importer_path, module_name, definition_path, repo_root, level=level
6420
6427
  )
6428
+ return bool(details["matched"]), list(details["provenance"])
6421
6429
 
6422
6430
 
6423
6431
  def _group_symbols_by_file(symbols: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
@@ -15463,6 +15471,7 @@ def _confirm_import_edges(
15463
15471
  # a confirmed edge for an import whose target we can't actually read (precision
15464
15472
  # matters more than recall past this point).
15465
15473
  continue
15474
+ python_provenance: list[str] = []
15466
15475
  if language_id in ("javascript", "typescript"):
15467
15476
  matched = _js_ts_module_matches_definition(
15468
15477
  candidate_importer, module, target_file, repo_root
@@ -15479,7 +15488,7 @@ def _confirm_import_edges(
15479
15488
  # `_python_module_candidates` resolve a relative import correctly instead of
15480
15489
  # falling back to a bare path-suffix match that ignores directory context.
15481
15490
  level = int(raw_entry.get("level", 0) or 0)
15482
- matched = _python_module_matches_definition(
15491
+ matched, python_provenance = _python_module_matches_definition(
15483
15492
  candidate_importer, module, target_file, repo_root, level=level
15484
15493
  )
15485
15494
  if not matched or line in seen_lines:
@@ -15496,6 +15505,16 @@ def _confirm_import_edges(
15496
15505
  "provenance": provenance,
15497
15506
  "resolution_confidence": _import_graph_resolution_confidence(provenance),
15498
15507
  }
15508
+ if python_provenance == ["sys-path-insert"]:
15509
+ # #155 fix: report the sys.path-hack tag honestly on the reverse edge too, mirroring
15510
+ # the forward `tg imports` path (`_resolve_raw_import_entry`). A SEPARATE field, not
15511
+ # an overwrite of `provenance` above -- that string drives
15512
+ # `_import_graph_resolution_confidence`'s enum ("parser-backed"/"heuristic"/
15513
+ # "regex-heuristic", pinned by test_import_span_targeting.py) and this edge is still
15514
+ # exactly as parser-confirmed (exact AST-parsed, exact resolved-path match) as any
15515
+ # other Python edge, just resolved via a sys.path-hacked root instead of a standard
15516
+ # one.
15517
+ edge["path_provenance"] = "sys-path-insert"
15499
15518
  if dynamic:
15500
15519
  # Payload-bloat fix (#93 SUB-1 follow-up, same rationale as
15501
15520
  # _resolve_raw_import_entry): preserve the dynamic markers on an edge that IS
@@ -84,6 +84,13 @@ def _read_project_version_fallback() -> str:
84
84
  return "0.0.0"
85
85
 
86
86
 
87
+ # Safe to cache (unlike `_native_tg_version` below, whose docstring at `inspect_native_tg_binary`
88
+ # warns against a path-keyed cache going stale across an `os.replace` swap): this function takes
89
+ # no arguments and only reads pyproject.toml + installed package metadata, neither of which change
90
+ # within a single process lifetime. Called on every daemon probe (`_probe_daemon` in
91
+ # session_daemon.py) since the daemon became default-on for symbol commands, so an uncached
92
+ # pyproject.toml read + importlib.metadata scan on every probe is wasted I/O.
93
+ @lru_cache(maxsize=1)
87
94
  def _expected_tg_version() -> str:
88
95
  source_version = _read_project_version_fallback()
89
96
  try:
File without changes
File without changes