From 65176d2165681111a4245bf2284a285755939bee Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 24 Sep 2026 01:34:46 +0000 Subject: [PATCH 1/3] fix(daemon): unify Windows project root identity --- crates/tracedecay-cli/src/lsp_cmd.rs | 9 +++------ crates/tracedecay/src/daemon/bootstrap_route.rs | 5 ++--- crates/tracedecay/src/daemon/engine.rs | 5 ++--- crates/tracedecay/src/daemon/project_routing.rs | 5 ++--- crates/tracedecay/src/daemon/tests/bootstrap.rs | 14 ++++++++++++++ .../storage_resolver_test/artifact_routing.rs | 4 ++-- 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/crates/tracedecay-cli/src/lsp_cmd.rs b/crates/tracedecay-cli/src/lsp_cmd.rs index dbce29db39..9b49dc20f4 100644 --- a/crates/tracedecay-cli/src/lsp_cmd.rs +++ b/crates/tracedecay-cli/src/lsp_cmd.rs @@ -556,7 +556,7 @@ mod tests { let binding = initialize_binding(&frame).expect("initialize binding"); assert_eq!( binding.project_root, - root.path().canonicalize().expect("canonical workspace") + tracedecay_runtime_core::path_safety::canonical_root_identity(root.path()) ); let forwarded: Value = serde_json::from_str(&binding.frame).expect("forwarded initialize frame"); @@ -630,11 +630,8 @@ mod tests { assert!(binding.workspace_folders.is_sorted()); assert_eq!( binding.project_root, - first - .path() - .canonicalize() - .unwrap() - .min(second.path().canonicalize().unwrap()) + tracedecay_runtime_core::path_safety::canonical_root_identity(first.path()) + .min(tracedecay_runtime_core::path_safety::canonical_root_identity(second.path())) ); let forwarded: Value = serde_json::from_str(&binding.frame).unwrap(); assert_eq!( diff --git a/crates/tracedecay/src/daemon/bootstrap_route.rs b/crates/tracedecay/src/daemon/bootstrap_route.rs index 47ecb3b2b7..8d46575e20 100644 --- a/crates/tracedecay/src/daemon/bootstrap_route.rs +++ b/crates/tracedecay/src/daemon/bootstrap_route.rs @@ -149,9 +149,8 @@ fn cached_project_node_count_inner<'a>( // so every profiling feature can compute its layout. Box::pin(async move { let project_path = handshake.project_path.as_ref()?; - let canonical_project_path = project_path - .canonicalize() - .unwrap_or_else(|_| project_path.clone()); + let canonical_project_path = + tracedecay_runtime_core::path_safety::canonical_root_identity(project_path); let route = ProjectRouteKey::from_handshake(&canonical_project_path, handshake).ok()?; let _server = { let servers = store_administration.project_servers().lock().await; diff --git a/crates/tracedecay/src/daemon/engine.rs b/crates/tracedecay/src/daemon/engine.rs index b629ba31bd..0c1c4bcda4 100644 --- a/crates/tracedecay/src/daemon/engine.rs +++ b/crates/tracedecay/src/daemon/engine.rs @@ -831,9 +831,8 @@ impl DaemonEngine { message: "project server requested without project_path".to_string(), }); }; - let canonical_project_path = project_path - .canonicalize() - .unwrap_or_else(|_| project_path.clone()); + let canonical_project_path = + tracedecay_runtime_core::path_safety::canonical_root_identity(project_path); Box::pin( self.ensure_registered_project_route(&canonical_project_path, handshake.allow_init), ) diff --git a/crates/tracedecay/src/daemon/project_routing.rs b/crates/tracedecay/src/daemon/project_routing.rs index 7de92ca006..17af494007 100644 --- a/crates/tracedecay/src/daemon/project_routing.rs +++ b/crates/tracedecay/src/daemon/project_routing.rs @@ -78,9 +78,8 @@ pub(super) fn project_route_for_handshake( message: "project server requested without project_path".to_string(), }); }; - let canonical_project_path = project_path - .canonicalize() - .unwrap_or_else(|_| project_path.clone()); + let canonical_project_path = + tracedecay_runtime_core::path_safety::canonical_root_identity(project_path); if crate::config::is_ambient_project_root(&canonical_project_path) { return Err(TraceDecayError::Config { message: format!( diff --git a/crates/tracedecay/src/daemon/tests/bootstrap.rs b/crates/tracedecay/src/daemon/tests/bootstrap.rs index c2c1d7c7af..bddff10781 100644 --- a/crates/tracedecay/src/daemon/tests/bootstrap.rs +++ b/crates/tracedecay/src/daemon/tests/bootstrap.rs @@ -199,6 +199,20 @@ fn daemon_project_route_rejects_the_user_profile_root() { assert!(error.to_string().contains("ambient user/filesystem root")); } +#[test] +fn daemon_project_route_uses_the_product_root_identity() { + let project = TempDir::new().expect("project root"); + let mut handshake = test_handshake_defaults(); + handshake.project_path = Some(project.path().to_path_buf()); + + let expected = tracedecay_runtime_core::path_safety::canonical_root_identity(project.path()); + let (project_root, route) = + super::super::project_route_for_handshake(&handshake).expect("resolve project route"); + + assert_eq!(project_root, expected); + assert_eq!(route.project_path, expected); +} + /// Enrolls `project_root` on disk exactly as a previously-initialized project /// is enrolled, a `.git/` repository identity marker plus a materialized /// profile store, without touching the profile registry. This is the on-disk diff --git a/crates/tracedecay/tests/storage_suite/storage_resolver_test/artifact_routing.rs b/crates/tracedecay/tests/storage_suite/storage_resolver_test/artifact_routing.rs index 73a2991870..6b84e5eac2 100644 --- a/crates/tracedecay/tests/storage_suite/storage_resolver_test/artifact_routing.rs +++ b/crates/tracedecay/tests/storage_suite/storage_resolver_test/artifact_routing.rs @@ -46,9 +46,9 @@ async fn hermes_profile_like_directory_uses_user_profile_shard() { let expected = home .join(".tracedecay") .join(format!("projects/{project_id}/sessions.db")); - assert_eq!( + assert_path_eq( resolve_project_session_db_path(&hermes_home).unwrap(), - expected + expected, ); } From d5585887f3b7878b587da89e937c2179babf07e9 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 24 Sep 2026 03:47:39 +0000 Subject: [PATCH 2/3] fix(daemon): normalize Windows route keys at storage boundaries --- crates/tracedecay/src/daemon/production_harness.rs | 2 ++ crates/tracedecay/src/daemon/project_open_admission.rs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/tracedecay/src/daemon/production_harness.rs b/crates/tracedecay/src/daemon/production_harness.rs index 7912b7cce5..39bd85c3cc 100644 --- a/crates/tracedecay/src/daemon/production_harness.rs +++ b/crates/tracedecay/src/daemon/production_harness.rs @@ -781,6 +781,8 @@ impl ProductionProjectCompositionHarnessV1 { ), } })?; + let canonical_project_path = + tracedecay_runtime_core::path_safety::plain_host_path(&canonical_project_path); self.resources .as_ref() .and_then(|resources| resources.servers.get(&canonical_project_path)) diff --git a/crates/tracedecay/src/daemon/project_open_admission.rs b/crates/tracedecay/src/daemon/project_open_admission.rs index c8cf52cc22..2d9b4b8199 100644 --- a/crates/tracedecay/src/daemon/project_open_admission.rs +++ b/crates/tracedecay/src/daemon/project_open_admission.rs @@ -1194,7 +1194,9 @@ impl ProjectRouteKey { global_db_path: authority::canonical_identity_path( &handshake.client_identity.global_db_path, )?, - project_path: authority::canonical_identity_path(project_path)?, + project_path: tracedecay_runtime_core::path_safety::plain_host_path( + &authority::canonical_identity_path(project_path)?, + ), scope_prefix: handshake.scope_prefix.clone(), }) } From 1265e9c1b5066474acd0475e46f383ea6b895756 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 24 Sep 2026 22:59:48 +0000 Subject: [PATCH 3/3] fix(daemon): key registry and code index by product root identity Windows canonicalize spells roots `\\?\D:\...`, while the daemon graph and route now carry `canonical_root_identity` (`D:\...`). The project registry and the code-index scheduler still re-canonicalized to the verbatim form, so a resolved hook route's owner root disagreed with its graph root, and maintenance hashed the plain layout root into a different code-index store than the scheduler mounted, so superseded generations were never collected. Store registry roots and every scheduler root, mount key, worktree id, scope hash, and containment check through the same identity (strictly, for paths that must exist), and derive `ProjectRouteKey` and the harness lookup from it instead of re-spelling a verbatim path at each boundary. --- crates/tracedecay-cli/src/lsp_cmd.rs | 9 +- .../src/code_index_scheduler/activation.rs | 8 +- .../code_index_scheduler/activation_tests.rs | 7 +- .../branch_generations.rs | 19 ++-- .../branch_publication.rs | 58 +++++------ .../src/code_index_scheduler/identity.rs | 14 +-- .../ignored_dependencies.rs | 16 ++- .../src/code_index_scheduler/reconcile.rs | 3 +- .../src/code_index_scheduler/registry.rs | 43 ++++---- .../registry/cold_read_wake_tests.rs | 3 +- .../registry/convergence_park_tests.rs | 8 +- .../registry/ignored_dependencies.rs | 3 +- .../registry/lsp_projection.rs | 4 +- .../code_index_scheduler/registry/mount.rs | 3 +- .../registry/query_authority.rs | 11 ++- .../reconcile_failure_isolation_tests.rs | 21 ++-- .../registry/serving_readiness_tests.rs | 3 +- .../registry/serving_reads.rs | 31 +++--- .../registry/test_gates.rs | 46 ++++----- .../src/code_index_scheduler/tests/mod.rs | 3 +- .../code_index_scheduler/tests/reconcile.rs | 99 ++++++++----------- .../tests/retained_configuration_tests.rs | 5 +- .../src/code_index_scheduler/tests/serving.rs | 28 +++--- .../src/git_watch.rs | 4 +- .../ignored_dependency_admission.rs | 12 ++- .../project_reads/scope_admission_tests.rs | 3 +- .../src/project_registry.rs | 2 +- .../src/path_safety.rs | 8 ++ ...e_index_runtime_generation_census_tests.rs | 4 +- ...de_index_runtime_graph_activation_tests.rs | 13 +-- .../src/daemon/production_harness.rs | 17 ++-- .../generation_retention_test.rs | 7 +- .../code_index_activation.rs | 13 +-- .../src/daemon/project_open_admission.rs | 4 +- .../ignored_dependency_admission_tests.rs | 3 +- .../cancellation_tests.rs | 6 +- .../flight_tests.rs | 3 +- .../tests/daemon_suite/code_index_journey.rs | 11 ++- .../daemon_suite/indexing_lifecycle_test.rs | 7 +- 39 files changed, 286 insertions(+), 276 deletions(-) diff --git a/crates/tracedecay-cli/src/lsp_cmd.rs b/crates/tracedecay-cli/src/lsp_cmd.rs index 9b49dc20f4..667ec21b44 100644 --- a/crates/tracedecay-cli/src/lsp_cmd.rs +++ b/crates/tracedecay-cli/src/lsp_cmd.rs @@ -505,6 +505,7 @@ fn print_lsp_servers_table(adapters: &[lsp_adapters::LspAdapterDefinition]) { #[cfg(test)] mod tests { use serde_json::{Value, json}; + use tracedecay_runtime_core::path_safety::canonical_root_identity; use super::{bridge_config_error, finish_stdio_bridge, initialize_binding}; @@ -554,10 +555,7 @@ mod tests { .to_string(); let binding = initialize_binding(&frame).expect("initialize binding"); - assert_eq!( - binding.project_root, - tracedecay_runtime_core::path_safety::canonical_root_identity(root.path()) - ); + assert_eq!(binding.project_root, canonical_root_identity(root.path())); let forwarded: Value = serde_json::from_str(&binding.frame).expect("forwarded initialize frame"); assert_eq!(forwarded["params"]["rootUri"], binding.canonical_root_uri); @@ -630,8 +628,7 @@ mod tests { assert!(binding.workspace_folders.is_sorted()); assert_eq!( binding.project_root, - tracedecay_runtime_core::path_safety::canonical_root_identity(first.path()) - .min(tracedecay_runtime_core::path_safety::canonical_root_identity(second.path())) + canonical_root_identity(first.path()).min(canonical_root_identity(second.path())) ); let forwarded: Value = serde_json::from_str(&binding.frame).unwrap(); assert_eq!( diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation.rs index 883de156fe..a3006529ef 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation.rs @@ -24,6 +24,7 @@ use super::demand_admission::{ CodeIndexDemandAdmissionV1, CodeIndexDemandUnavailableV1, CodeIndexDemandV1, }; use super::identity::IndexingIdentityV1; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; const ACTIVATION_IDLE: u8 = 0; const ACTIVATION_MOUNTING: u8 = 1; @@ -169,8 +170,7 @@ impl CodeIndexActivationV1 { mount: CodeIndexActivationMountV1, hint_sink: CodeIndexActivationHintSinkV1, ) -> Self { - let project_root = project_root - .canonicalize() + let project_root = canonical_existing_identity(project_root) .unwrap_or_else(|_| project_root.to_path_buf()); let identity = Arc::new(Mutex::new(IndexingIdentityV1::resolve(&project_root).ok())); Self { @@ -199,9 +199,7 @@ impl CodeIndexActivationV1 { } fn accepts_root(&self, project_root: &Path) -> bool { - project_root - .canonicalize() - .is_ok_and(|root| root == self.project_root) + canonical_existing_identity(project_root).is_ok_and(|root| root == self.project_root) } pub fn identity(&self) -> Option { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation_tests.rs index 8e50328c1e..6176720ccf 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/activation_tests.rs @@ -29,6 +29,7 @@ use super::{ DaemonCodeIndexPublicationStoreV1, SharedCodeIndexBytePoolV1, }; use tracedecay_privacy::CODE_SOURCE_SANITIZER_VERSION_V1; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; fn git(root: &Path, args: &[&str]) { let output = Command::new("git") @@ -123,10 +124,8 @@ fn publication_store(store_root: &Path) -> DaemonCodeIndexPublicationStoreV1 { async fn cold_mount_defers_sealed_decode_and_truth_verification_to_the_retained_owner() { let project = fixture(); let store = TempDir::new().expect("store root"); - let canonical_project_root = project - .path() - .canonicalize() - .expect("canonical project root"); + let canonical_project_root = + canonical_existing_identity(project.path()).expect("canonical project root"); let scoped_store = super::scoped_code_index_store_root(store.path(), &canonical_project_root); let generation_id = { let mut scheduler = open(project.path(), &scoped_store); diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_generations.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_generations.rs index 2a737daa07..f529737a47 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_generations.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_generations.rs @@ -579,6 +579,7 @@ mod tests { use crate::code_index_scheduler::{ CodeIndexWorktreeSchedulerV1, SharedCodeIndexBytePoolV1, scoped_code_index_store_root, }; + use tracedecay_runtime_core::path_safety::canonical_existing_identity; fn git(root: &Path, args: &[&str]) -> String { let output = Command::new("git") @@ -620,7 +621,8 @@ mod tests { let base_tree = GitOidV1::new(git(project.path(), &["rev-parse", "HEAD^{tree}"])).expect("base tree"); let project_id = ProjectId::new("project.branch-generation-diff").expect("project id"); - let canonical_project = project.path().canonicalize().expect("canonical project"); + let canonical_project = + canonical_existing_identity(project.path()).expect("canonical project"); let scoped_store = scoped_code_index_store_root(store.path(), &canonical_project); let mut scheduler = CodeIndexWorktreeSchedulerV1::open( project_id.clone(), @@ -978,7 +980,8 @@ mod tests { git(project.path(), &["checkout", "-q", "main"]); let project_id = ProjectId::new("project.non-checked-out-refs").expect("project id"); - let canonical_project = project.path().canonicalize().expect("canonical project"); + let canonical_project = + canonical_existing_identity(project.path()).expect("canonical project"); let scoped_store = scoped_code_index_store_root(store.path(), &canonical_project); let scheduler = CodeIndexWorktreeSchedulerV1::open( project_id.clone(), @@ -1094,7 +1097,8 @@ mod tests { .expect("dirty source"); let project_id = ProjectId::new("project.dirty-generation").expect("project id"); - let canonical_project = project.path().canonicalize().expect("canonical project"); + let canonical_project = + canonical_existing_identity(project.path()).expect("canonical project"); let scoped_store = scoped_code_index_store_root(store.path(), &canonical_project); let mut scheduler = CodeIndexWorktreeSchedulerV1::open( project_id.clone(), @@ -1303,7 +1307,8 @@ mod tests { ); let project_id = ProjectId::new("project.superseded-tip-mint").expect("project id"); - let canonical_project = project.path().canonicalize().expect("canonical project"); + let canonical_project = + canonical_existing_identity(project.path()).expect("canonical project"); let scoped_store = scoped_code_index_store_root(store.path(), &canonical_project); // Only the current tip is indexed, so the pair's head is served from the // index and its base, a commit the branch has already left behind, is @@ -1432,7 +1437,8 @@ mod tests { ); let project_id = ProjectId::new(project_id).expect("project id"); - let canonical_project = project.path().canonicalize().expect("canonical project"); + let canonical_project = + canonical_existing_identity(project.path()).expect("canonical project"); let scoped_store = scoped_code_index_store_root(store.path(), &canonical_project); let mut scheduler = CodeIndexWorktreeSchedulerV1::open( project_id.clone(), @@ -1689,7 +1695,8 @@ mod tests { ); let project_id = ProjectId::new("project.truncated-index-mint").expect("project id"); - let canonical_project = project.path().canonicalize().expect("canonical project"); + let canonical_project = + canonical_existing_identity(project.path()).expect("canonical project"); let scoped_store = scoped_code_index_store_root(store.path(), &canonical_project); let mut scheduler = CodeIndexWorktreeSchedulerV1::open( project_id.clone(), diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs index 0be35b56ba..cb9c58ef6f 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/branch_publication.rs @@ -24,6 +24,7 @@ use super::{ CodeIndexDemandAdmissionV1, CodeIndexPublishedGenerationV1, CodeIndexSchedulerRegistryV1, ServingGenerationInstallationOutcomeV1, ServingGenerationRollbackOutcomeV1, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; const CODE_INDEX_SCHEDULER_UNAVAILABLE: &str = "code_index_scheduler_unavailable"; const CODE_INDEX_ACTIVATION_UNAVAILABLE: &str = "code_index_activation_unavailable"; @@ -116,16 +117,17 @@ impl BranchPublicationContextV1 { branch: &str, cancellation: &CancellationToken, ) -> Result { - let canonical_project_root = project_root.canonicalize().map_err(|error| { - TraceDecayError::project_route( - CODE_INDEX_IDENTITY_MISMATCH, - false, - format!( - "failed to canonicalize branch project root '{}': {error}", - project_root.display() - ), - ) - })?; + let canonical_project_root = + canonical_existing_identity(project_root).map_err(|error| { + TraceDecayError::project_route( + CODE_INDEX_IDENTITY_MISMATCH, + false, + format!( + "failed to canonicalize branch project root '{}': {error}", + project_root.display() + ), + ) + })?; if !self.owns_project(&canonical_project_root)? { return Err(TraceDecayError::project_route( CODE_INDEX_IDENTITY_MISMATCH, @@ -139,16 +141,17 @@ impl BranchPublicationContextV1 { if cancellation.is_cancelled() { return Err(branch_publication_cancelled_error(branch)); } - let canonical_worktree_root = worktree_root.canonicalize().map_err(|error| { - TraceDecayError::project_route( - CODE_INDEX_IDENTITY_MISMATCH, - false, - format!( - "failed to canonicalize branch worktree '{}': {error}", - worktree_root.display() - ), - ) - })?; + let canonical_worktree_root = + canonical_existing_identity(worktree_root).map_err(|error| { + TraceDecayError::project_route( + CODE_INDEX_IDENTITY_MISMATCH, + false, + format!( + "failed to canonicalize branch worktree '{}': {error}", + worktree_root.display() + ), + ) + })?; let source_branch = tracedecay_runtime_core::branch::current_branch( &canonical_worktree_root, ) @@ -634,15 +637,12 @@ impl BranchPublicationContextV1 { } fn owns_project(&self, canonical_root: &Path) -> Result { - let retained_root = - self.project_root - .canonicalize() - .map_err(|error| TraceDecayError::File { - message: format!( - "failed to canonicalize retained branch project root: {error}" - ), - path: self.project_root.display().to_string(), - })?; + let retained_root = canonical_existing_identity(&self.project_root).map_err(|error| { + TraceDecayError::File { + message: format!("failed to canonicalize retained branch project root: {error}"), + path: self.project_root.display().to_string(), + } + })?; Ok(retained_root == canonical_root) } } diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs index 2c2da16c3c..f556e92922 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/identity.rs @@ -16,6 +16,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tracedecay_contracts::{ApplicationContractError, ResolvedScope}; use tracedecay_domain::{CommitId, ProjectId, RefId, RepositoryId, TreeId, WorktreeId}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; /// Failure to resolve an exact indexing identity from a checkout. #[derive(Debug, thiserror::Error)] @@ -315,13 +316,12 @@ pub fn repository_id_for_common_dir(common_dir: &Path) -> Result Result { - let project_root = - project_root - .canonicalize() - .map_err(|source| IdentityErrorV1::CanonicalWorktreePath { - path: project_root.to_path_buf(), - source, - })?; + let project_root = canonical_existing_identity(project_root).map_err(|source| { + IdentityErrorV1::CanonicalWorktreePath { + path: project_root.to_path_buf(), + source, + } + })?; WorktreeId::new(format!( "worktree.daemon.{}", super::sha256_hex(project_root.to_string_lossy().as_bytes()) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/ignored_dependencies.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/ignored_dependencies.rs index b75f41f6c2..10d6fa998f 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/ignored_dependencies.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/ignored_dependencies.rs @@ -24,6 +24,7 @@ use super::{ StaticLanguageRegistry, now_micros, projection_key, }; use crate::code_index::languages::LanguageRegistry; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; pub const ADMITTED_SOURCE_READ_CHUNK_BYTES: usize = 64 * 1024; @@ -392,7 +393,7 @@ impl CodeIndexWorktreeSchedulerV1 { .iter() .all(|admission| { let absolute = self.project_root.join(&admission.logical_path); - let Ok(canonical) = absolute.canonicalize() else { + let Ok(canonical) = canonical_existing_identity(&absolute) else { return false; }; if !canonical.starts_with(&self.project_root) { @@ -470,8 +471,7 @@ fn resolve_package_entrypoint( checkpoint_if_present(control)?; let package_json = package_root.join("package.json"); if package_json.is_file() { - let canonical_package_json = package_json - .canonicalize() + let canonical_package_json = canonical_existing_identity(&package_json) .map_err(|_| CodeIndexIgnoredDependencyRefusalV1::UnsupportedImport)?; if !canonical_package_json.starts_with(canonical_package) { return Err(CodeIndexIgnoredDependencyRefusalV1::SymlinkEscape.into()); @@ -554,9 +554,7 @@ fn read_contained_project_source( { return Err(CodeIndexIgnoredDependencyRefusalV1::PathEscape.into()); } - let canonical = project_root - .join(relative) - .canonicalize() + let canonical = canonical_existing_identity(&project_root.join(relative)) .map_err(|_| CodeIndexIgnoredDependencyRefusalV1::PathEscape)?; if !canonical.starts_with(project_root) { return Err(CodeIndexIgnoredDependencyRefusalV1::PathEscape.into()); @@ -571,8 +569,7 @@ fn canonical_package_root( project_root: &Path, package_root: &Path, ) -> Result { - let canonical = package_root - .canonicalize() + let canonical = canonical_existing_identity(package_root) .map_err(|_| CodeIndexIgnoredDependencyRefusalV1::UnsupportedImport)?; if !canonical.starts_with(project_root) || canonical != package_root { return Err(CodeIndexIgnoredDependencyRefusalV1::SymlinkEscape.into()); @@ -634,8 +631,7 @@ fn validate_admitted_source( control: Option<&dyn CodeIndexExecutionControlV1>, ) -> Result, CodeIndexSchedulerErrorV1> { checkpoint_if_present(control)?; - let canonical_entrypoint = entrypoint - .canonicalize() + let canonical_entrypoint = canonical_existing_identity(entrypoint) .map_err(|_| CodeIndexIgnoredDependencyRefusalV1::UnsupportedImport)?; if !canonical_entrypoint.starts_with(project_root) || !canonical_entrypoint.starts_with(canonical_package) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs index 20d08a31a6..a83c866680 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs @@ -72,6 +72,7 @@ use super::{ }; #[cfg(test)] use super::{HeldActiveDecodeV1, reconcile_panic_guard}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; const MAX_PENDING_HINTS: usize = 1_024; const MAX_SUPERSEDED_RECONCILE_RETRIES: usize = 4; @@ -949,7 +950,7 @@ impl CodeIndexWorktreeSchedulerV1 { byte_pool: Arc, policy: CodeIndexHintPolicyV1, ) -> Result { - let project_root = project_root.canonicalize()?; + let project_root = canonical_existing_identity(project_root)?; // Resolve exact identity BEFORE any indexing work. Paths located this // checkout; identity authorizes what may be reused. let identity = identity::IndexingIdentityV1::resolve(&project_root) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs index d8aa4ffeeb..3b632a139e 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs @@ -42,6 +42,7 @@ use super::{ LatestCompleteCodeIndexV1, PendingHintsV1, SharedCodeIndexBytePoolV1, newly_eligible_percentile, now_micros, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[cfg(test)] mod cold_read_wake_tests; @@ -1912,7 +1913,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, expected: &Arc, ) -> ServingGenerationInstallationOutcomeV1 { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return ServingGenerationInstallationOutcomeV1::NoMatch; }; let (serving_generation, serving_epoch, installation_slot) = { @@ -1985,7 +1986,7 @@ impl CodeIndexSchedulerRegistryV1 { installation: &ServingGenerationInstallationClaimV1, retire: bool, ) -> ServingGenerationRollbackOutcomeV1 { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return ServingGenerationRollbackOutcomeV1::NoMatch; }; let ( @@ -2619,7 +2620,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; let worktree = mounted.get(&project_root)?; Some(worktree.serving_generation_changed.subscribe()) @@ -2629,7 +2630,7 @@ impl CodeIndexSchedulerRegistryV1 { /// notes a [`CodeIndexCadenceTriggerV1::QueryAdmission`] wake so the worker /// yields text-only work and seats a complete generation. pub async fn request_complete_generation(&self, project_root: &Path) -> bool { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return false; }; let mounted = self.mounted.lock().await; @@ -2777,7 +2778,7 @@ impl CodeIndexSchedulerRegistryV1 { /// `false` when the path cannot be canonicalized (a path Doctor could never /// have mounted under). pub async fn is_worktree_mounted(&self, project_root: &Path) -> bool { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return false; }; self.mounted.lock().await.contains_key(&project_root) @@ -2801,7 +2802,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, path: PathBuf, ) -> CodeIndexDemandAdmissionV1 { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return CodeIndexDemandAdmissionV1::Unavailable( CodeIndexDemandUnavailableV1::SchedulerUnmounted, ); @@ -2842,7 +2843,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, rel_paths: &[String], ) -> CodeIndexDemandAdmissionV1 { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return CodeIndexDemandAdmissionV1::Unavailable( CodeIndexDemandUnavailableV1::SchedulerUnmounted, ); @@ -2888,7 +2889,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return None; }; let mounted = self.mounted.lock().await; @@ -2906,7 +2907,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, reason: &str, ) -> bool { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return false; }; let mounted = self.mounted.lock().await; @@ -2934,7 +2935,7 @@ impl CodeIndexSchedulerRegistryV1 { /// bounded exact-path capacity. Overflow requests one authoritative scan for /// this exact mounted worktree; it never aliases a sibling worktree. pub async fn notify_hook_overflow(&self, project_root: &Path) -> CodeIndexDemandAdmissionV1 { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return CodeIndexDemandAdmissionV1::Unavailable( CodeIndexDemandUnavailableV1::SchedulerUnmounted, ); @@ -2978,7 +2979,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> CodeIndexDemandAdmissionV1 { - let Ok(canonical) = project_root.canonicalize() else { + let Ok(canonical) = canonical_existing_identity(project_root) else { return CodeIndexDemandAdmissionV1::Unavailable( CodeIndexDemandUnavailableV1::SchedulerUnmounted, ); @@ -3014,7 +3015,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option>> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; mounted .get(&project_root) @@ -3216,7 +3217,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, scope: &tracedecay_contracts::ResolvedScope, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted_root = { let mounted = self.mounted.lock().await; let (mounted_root, _) = unique_mounted_for_scope(&mounted, scope).unique()?; @@ -3248,7 +3249,7 @@ impl ScopedFeedbackDocumentIdentityV1 { ) -> Option { Some(Self { registry, - project_root: project_root.canonicalize().ok()?, + project_root: canonical_existing_identity(project_root).ok()?, scope, }) } @@ -3265,8 +3266,7 @@ impl tracedecay_application::feedback::cycle_production::ProductionFeedbackDocum { let owner = self.clone(); Box::pin(async move { - let requested_root = project_root - .canonicalize() + let requested_root = canonical_existing_identity(&project_root) .map_err(|_| LspRuntimeFailure::new("feedback-code-index-root-unavailable"))?; if requested_root != owner.project_root { return Err(LspRuntimeFailure::new("feedback-code-index-root-mismatch")); @@ -3351,7 +3351,7 @@ impl CodeIndexSchedulerRegistryV1 { scope: Option, ) -> Option { - let root = project_root.canonicalize().ok()?; + let root = canonical_existing_identity(&project_root).ok()?; let root_generation = self.latest_text_serving_for_root(&root).await?; let scope = match scope { Some(scope) => scope, @@ -3512,7 +3512,7 @@ fn canonical_relative_document_path(project_root: &Path, path: &Path) -> Option< let mut unresolved: Vec<&std::ffi::OsStr> = Vec::new(); let mut candidate = path; loop { - if let Ok(canonical) = candidate.canonicalize() { + if let Ok(canonical) = canonical_existing_identity(candidate) { let mut relative = canonical.strip_prefix(project_root).ok()?.to_path_buf(); for component in unresolved.iter().rev() { relative.push(component); @@ -3527,6 +3527,7 @@ fn canonical_relative_document_path(project_root: &Path, path: &Path) -> Option< #[cfg(all(test, unix))] mod feedback_document_path_tests { use super::feedback_document_logical_path; + use tracedecay_runtime_core::path_safety::canonical_existing_identity; /// A symlinked root reproduces on Linux exactly what every macOS /// `/var/folders/...` temporary root does in production: the daemon holds @@ -3541,7 +3542,7 @@ mod feedback_document_path_tests { let alias = base.path().join("alias"); std::os::unix::fs::symlink(&real, &alias).expect("root alias"); - let canonical_root = real.canonicalize().expect("canonical root"); + let canonical_root = canonical_existing_identity(&real).expect("canonical root"); let canonical_uri = url::Url::from_file_path(canonical_root.join("src/lib.rs")) .expect("canonical document uri"); let alias_uri = @@ -3568,7 +3569,7 @@ mod feedback_document_path_tests { std::fs::create_dir_all(real.join("src")).expect("real tree"); let alias = base.path().join("alias"); std::os::unix::fs::symlink(&real, &alias).expect("root alias"); - let canonical_root = real.canonicalize().expect("canonical root"); + let canonical_root = canonical_existing_identity(&real).expect("canonical root"); let uri = url::Url::from_file_path(alias.join("src/unsaved.rs")).expect("document uri"); assert_eq!( @@ -3589,7 +3590,7 @@ mod feedback_document_path_tests { std::fs::create_dir_all(&outside).expect("outside tree"); std::fs::write(outside.join("secret.rs"), b"pub fn secret() {}\n").expect("outside file"); std::os::unix::fs::symlink(&outside, real.join("escape")).expect("escaping alias"); - let canonical_root = real.canonicalize().expect("canonical root"); + let canonical_root = canonical_existing_identity(&real).expect("canonical root"); let escaping = url::Url::from_file_path(canonical_root.join("escape/secret.rs")) .expect("escaping document uri"); diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/cold_read_wake_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/cold_read_wake_tests.rs index dac7eadeed..dc72a4e2bc 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/cold_read_wake_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/cold_read_wake_tests.rs @@ -13,6 +13,7 @@ use super::super::{ }; use super::{CodeIndexReconcileAdmissionV1, CodeIndexSchedulerRegistryV1}; use crate::code_index::production::CodeIndexExecutionControlV1; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[tokio::test] async fn cold_read_wakes_do_not_cancel_an_in_flight_reconcile_snapshot() { @@ -36,7 +37,7 @@ async fn cold_read_wakes_do_not_cancel_an_in_flight_reconcile_snapshot() { .mount_worktree(project_id.clone(), &project, fixture.path().join("store")) .await .expect("mount scheduler"); - let canonical_project = project.canonicalize().expect("canonical project"); + let canonical_project = canonical_existing_identity(&project).expect("canonical project"); let (scope, scheduler, hints, epoch, shutting_down, reconcile_in_progress) = { let mounted = registry.mounted.lock().await; diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs index caa6884abb..14413122a2 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/convergence_park_tests.rs @@ -31,6 +31,7 @@ use super::super::graph_activation::{ set_injected_activation_failures, }; use super::CodeIndexSchedulerRegistryV1; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; /// Ceiling on how long a test waits for the worker to reach the asserted /// state. Nothing is asserted about elapsed time; this only stops a hung @@ -89,7 +90,8 @@ impl Fixture { // is the one `poison` plants on the artifacts root itself. let store = root.path().join("store"); tracedecay_private_fs::create_private_directory(&store).expect("create store root"); - let canonical_project = project.canonicalize().expect("canonical project root"); + let canonical_project = + canonical_existing_identity(&project).expect("canonical project root"); let scoped = scoped_code_index_store_root(&store, &canonical_project); tracedecay_private_fs::create_private_directory(&scoped).expect("create scoped root"); let artifacts_root = code_text_artifacts_root(&scoped); @@ -124,7 +126,7 @@ impl Fixture { /// One wake that carries no new input, exactly like the periodic cadence /// traffic a live daemon produces over an unchanged checkout. async fn wake_without_new_input(&self) { - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; if let Some(worktree) = mounted.get(&canonical) { worktree.wake.notify_one(); @@ -393,7 +395,7 @@ async fn fresh_graph_activation_starts_while_the_clone_successor_is_pending() { tokio::time::timeout(CONVERGENCE_DEADLINE, gate.wait_until_started()) .await .expect("fresh graph activation starts once exact and lexical owners are ready"); - let canonical = fixture.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&fixture.project).expect("canonical project"); let text = { let mounted = fixture.registry.mounted.lock().await; mounted diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/ignored_dependencies.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/ignored_dependencies.rs index d9b8c06b58..37b1f89c45 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/ignored_dependencies.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/ignored_dependencies.rs @@ -19,6 +19,7 @@ use crate::code_index_scheduler::{ CodeIndexSchedulerErrorV1, DaemonCodeIndexControlV1, LatestCompleteCodeIndexV1, PendingHintsV1, ReconcilePassGuard, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct AdmissionFlightKeyV1 { @@ -399,7 +400,7 @@ impl CodeIndexSchedulerRegistryV1 { request: CodeIndexIgnoredDependencyRequestV1, control: Arc, ) -> Result { - let project_root = project_root.canonicalize()?; + let project_root = canonical_existing_identity(project_root)?; let flight_key = AdmissionFlightKeyV1::for_request(&request)?; let ( repository_id, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/lsp_projection.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/lsp_projection.rs index 0686a40b92..5caf34ff6b 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/lsp_projection.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/lsp_projection.rs @@ -4,6 +4,7 @@ use tracedecay_lsp::{LspRuntimeFailure, LspRuntimeFuture}; use super::super::identity::IndexingIdentityV1; use super::CodeIndexSchedulerRegistryV1; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; impl tracedecay_application::lsp_runtime::LspCodeIndexProjectionIdentityPort for CodeIndexSchedulerRegistryV1 @@ -20,8 +21,7 @@ impl tracedecay_application::lsp_runtime::LspCodeIndexProjectionIdentityPort > { let registry = self.clone(); Box::pin(async move { - let root = project_root - .canonicalize() + let root = canonical_existing_identity(&project_root) .map_err(|_| LspRuntimeFailure::new("lsp-code-index-root-unavailable"))?; let identity_root = root.clone(); let live_identity = diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs index a8149314f1..f7741c5801 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs @@ -37,6 +37,7 @@ use super::{ convergence_park_retries_on_wake, is_repeated_conflict_verdict, park_convergence, publication_authority_is_terminal, retained_noop_requires_follow_up_wake, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; impl CodeIndexSchedulerRegistryV1 { #[cfg(test)] @@ -157,7 +158,7 @@ impl CodeIndexSchedulerRegistryV1 { store_root: PathBuf, graph_activation: CodeGraphActivationAuthorityV1, ) -> Result { - let project_root = project_root.canonicalize()?; + let project_root = canonical_existing_identity(project_root)?; #[cfg(test)] Self::pause_cold_mount_admission_for_test(&project_root).await; let cold_mount_reservation = loop { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/query_authority.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/query_authority.rs index 7004cb8328..4e95ef3245 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/query_authority.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/query_authority.rs @@ -4,6 +4,7 @@ use std::{path::Path, sync::Arc}; use super::super::{CodeIndexSchedulerErrorV1, LatestCompleteCodeIndexV1}; use super::{CodeIndexSchedulerRegistryV1, unique_mounted_for_scope}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; impl CodeIndexSchedulerRegistryV1 { pub(super) async fn install_test_attribution_authority( @@ -11,7 +12,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, latest: &LatestCompleteCodeIndexV1, ) -> bool { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return false; }; let Ok(authority) = latest.test_attribution_authority() else { @@ -48,7 +49,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return; }; self.test_attribution_authorities @@ -69,7 +70,7 @@ impl CodeIndexSchedulerRegistryV1 { scope .validate() .map_err(|error| CodeIndexSchedulerErrorV1::Identity(error.to_string()))?; - let project_root = project_root.canonicalize()?; + let project_root = canonical_existing_identity(project_root)?; let mut mounted = self.mounted.lock().await; let worktree = mounted.get_mut(&project_root).ok_or_else(|| { CodeIndexSchedulerErrorV1::Identity( @@ -104,7 +105,7 @@ impl CodeIndexSchedulerRegistryV1 { scope .validate() .map_err(|error| CodeIndexSchedulerErrorV1::Identity(error.to_string()))?; - let project_root = project_root.canonicalize()?; + let project_root = canonical_existing_identity(project_root)?; let mut mounted = self.mounted.lock().await; let target = mounted.get(&project_root).ok_or_else(|| { CodeIndexSchedulerErrorV1::Identity( @@ -179,7 +180,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, observability: super::super::observability::CodeIndexObservabilityV1, ) -> Result<(), CodeIndexSchedulerErrorV1> { - let project_root = project_root.canonicalize()?; + let project_root = canonical_existing_identity(project_root)?; let mounted = self.mounted.lock().await; let worktree = mounted.get(&project_root).ok_or_else(|| { CodeIndexSchedulerErrorV1::Identity( diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs index b79c0d41ab..6eb47413fc 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/reconcile_failure_isolation_tests.rs @@ -24,6 +24,7 @@ use super::super::{ }, }; use super::CodeIndexSchedulerRegistryV1; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; /// Wake rounds driven from outside the worker. Each stands for the ordinary /// wake traffic a live daemon produces (cadence ticks, queries, sibling @@ -116,7 +117,7 @@ impl Fixture { faulting_passes: usize, ) -> Arc { let fault = Arc::new(ReconcileFaultInjectionV1::new(kind, faulting_passes)); - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); worktree @@ -131,7 +132,7 @@ impl Fixture { /// exactly as it does not when a cadence tick or query wakes the worker /// over bytes nobody touched. async fn wake_without_new_input(&self) { - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); worktree.wake.notify_one(); @@ -139,7 +140,7 @@ impl Fixture { /// One attributable wake with no epoch advance. async fn wake_with_pending_arrival(&self) { - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); CodeIndexSchedulerRegistryV1::note_wake( @@ -150,7 +151,7 @@ impl Fixture { } async fn pending_wake_micros(&self) -> u64 { - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); let pending = worktree @@ -162,7 +163,7 @@ impl Fixture { } async fn clear_build_progress(&self) { - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); *worktree @@ -173,7 +174,7 @@ impl Fixture { } async fn clear_convergence_park_for_test(&self) { - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); *worktree @@ -186,7 +187,7 @@ impl Fixture { use tracedecay_contracts::code_index_freshness::{ CodeIndexBuildBlockedReasonV1, CodeIndexConvergenceParkedV1, }; - let canonical = self.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&self.project).expect("canonical project"); let mounted = self.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); *worktree @@ -727,7 +728,7 @@ async fn terminal_publication_park_stops_the_worker_without_a_local_latch() { async fn park_visible_before_progress_reason_returns_terminal_admission() { let fixture = Fixture::mount("project.reconcile-park-before-progress").await; let scope = { - let canonical = fixture.project.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(&fixture.project).expect("canonical project"); let mounted = fixture.registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); ResolvedScope::new( @@ -789,7 +790,7 @@ async fn cold_terminal_park_makes_the_freshness_probe_terminal() { let scope = { let mounted = fixture.registry.mounted.lock().await; let worktree = mounted - .get(&fixture.project.canonicalize().unwrap()) + .get(&canonical_existing_identity(&fixture.project).unwrap()) .unwrap(); ResolvedScope::new( worktree.project_id.clone(), @@ -827,7 +828,7 @@ async fn retire_and_remount_clears_terminal_publication_park_for_new_admission() )); let mut roots = std::collections::BTreeSet::new(); - roots.insert(fixture.project.canonicalize().expect("canonical project")); + roots.insert(canonical_existing_identity(&fixture.project).expect("canonical project")); assert!( fixture.registry.retire_project_roots(&roots).await, "retire must drain the terminal owner" diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs index bef2b22ba2..cc613b2841 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs @@ -15,6 +15,7 @@ use super::{ CodeIndexCadenceOutcomeV1, CodeIndexSchedulerRegistryV1, dashboard_generation_is_ready, serving_seat_matches_advertised_generation, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; /// Failure bound on an owner pass finishing once the worker is parked. Nothing /// here passes because time elapsed; a pass that never ends fails loudly. @@ -250,7 +251,7 @@ async fn serving_waiter_tracks_installation_freshness_and_retirement() { .iter() .any(|symbol| symbol.simple_name == "branch_probe") ); - let canonical_project = project.canonicalize().expect("canonical project"); + let canonical_project = canonical_existing_identity(&project).expect("canonical project"); let freshness = { let mounted = registry.mounted.lock().await; mounted diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs index cc6195496e..18acdbb8d5 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs @@ -23,6 +23,7 @@ use super::{ dashboard_code_graph_serving, dashboard_freshness_identity, dashboard_terminal_status, dashboard_text_freshness_identity, unique_mounted_for_scope, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; impl CodeIndexSchedulerRegistryV1 { /// Return the mounted scheduler's canonical worktree-change generation. @@ -34,7 +35,7 @@ impl CodeIndexSchedulerRegistryV1 { /// Until that ladder runs, callers intentionally receive the preceding /// generation and must not derive a parallel workspace fingerprint. pub async fn diagnostics_change_generation(&self, project_root: &Path) -> Option { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return None; }; let (scheduler, source_freshness, hints, wake, pending_wake, reconcile_in_progress, epoch) = { @@ -104,7 +105,7 @@ impl CodeIndexSchedulerRegistryV1 { /// runtime (generation retention) resolve through this read instead of /// re-deriving repository/worktree identity themselves. pub async fn serving_code_scope(&self, project_root: &Path) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let (repository_id, worktree_id, shutting_down, serving) = { let mounted = self.mounted.lock().await; let worktree = mounted.get(&project_root)?; @@ -129,7 +130,7 @@ impl CodeIndexSchedulerRegistryV1 { } pub async fn mounted_code_scope(&self, project_root: &Path) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; let worktree = mounted.get(&project_root)?; Some(CodeIndexMountedScopeV1 { @@ -151,7 +152,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, generation: &CodeGenerationId, ) -> Option> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let historical = { let mounted = self.mounted.lock().await; mounted @@ -179,7 +180,7 @@ impl CodeIndexSchedulerRegistryV1 { generation_id: &CodeGenerationId, ) -> Option>, CodeIndexSchedulerErrorV1>> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let owner = { let mounted = self.mounted.lock().await; mounted @@ -200,7 +201,7 @@ impl CodeIndexSchedulerRegistryV1 { } pub async fn latest_generation_id(&self, project_root: &Path) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; // Read the O(1) serving slot instead of the scheduler mutex. This used // to take `scheduler.lock()`, a blocking std mutex held by any // in-flight reconcile, while still holding the `mounted` async mutex, @@ -262,7 +263,7 @@ impl CodeIndexSchedulerRegistryV1 { Option, tracedecay_contracts::code_index_freshness::CodeIndexFreshnessReadFailureV1, > { - let canonical_root = match project_root.canonicalize() { + let canonical_root = match canonical_existing_identity(project_root) { Ok(root) => root, Err(_) => return Ok(None), }; @@ -513,7 +514,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option { - let canonical_root = project_root.canonicalize().ok()?; + let canonical_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; let worktree = mounted.get(&canonical_root)?; worktree @@ -531,7 +532,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; // Clone the per-worktree handle under a short map lock, then drop the // registry guard before checking the mounted route. let ( @@ -693,7 +694,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, admission: GenerationDecodeAdmissionV1, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let ( source_freshness, serving_generation, @@ -862,7 +863,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, scope: &tracedecay_contracts::ResolvedScope, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; // A synchronous census abstains under map contention; the verified // read path awaits the map instead (see // [`Self::latest_complete_ready_decoded_for_root_scope`]). @@ -979,7 +980,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, scope: &tracedecay_contracts::ResolvedScope, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; // Await the map mutex rather than try-locking it: its critical // sections are brief map reads, while an abstention under contention // here falsely demotes a proven-current answer to the stale serving @@ -1213,7 +1214,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, require_serving_ready: bool, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let text_generation = { let mounted = self.mounted.lock().await; Arc::clone(&mounted.get(&project_root)?.text_generation) @@ -1356,7 +1357,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, scope: &tracedecay_contracts::ResolvedScope, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let serving_generation = { let mounted = self.mounted.lock().await; let worktree = mounted.get(&project_root)?; @@ -1389,7 +1390,7 @@ impl CodeIndexSchedulerRegistryV1 { project_root: &Path, scope: &tracedecay_contracts::ResolvedScope, ) -> bool { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return false; }; let mounted = self.mounted.lock().await; diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/test_gates.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/test_gates.rs index 0159a9363f..0c760d060b 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/test_gates.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/test_gates.rs @@ -20,6 +20,7 @@ use super::{ cold_mount_post_check_controls, published_text_projection_gate, query_admission_controls, unique_mounted_for_scope, wait_notified_if_unset, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; impl CodeIndexSchedulerRegistryV1 { #[cfg(test)] @@ -63,7 +64,7 @@ impl CodeIndexSchedulerRegistryV1 { /// Test-only observation of an exact mounted worktree's active owner pass. #[cfg(test)] pub async fn reconcile_in_progress_for_test(&self, project_root: &Path) -> bool { - let Ok(project_root) = project_root.canonicalize() else { + let Ok(project_root) = canonical_existing_identity(project_root) else { return false; }; let reconcile_in_progress = self @@ -84,7 +85,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let reconcile_in_progress = self .mounted .lock() @@ -102,7 +103,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let serving = { let mounted = self.mounted.lock().await; Arc::clone(&mounted.get(&project_root)?.serving_generation) @@ -115,9 +116,8 @@ impl CodeIndexSchedulerRegistryV1 { #[cfg(test)] pub fn install_cold_mount_admission_barrier(&self, project_root: &Path, callers: usize) { - let project_root = project_root - .canonicalize() - .expect("canonical test project root"); + let project_root = + canonical_existing_identity(project_root).expect("canonical test project root"); let barrier = Arc::new(tokio::sync::Barrier::new(callers)); let replaced = cold_mount_admission_barriers() .lock() @@ -128,9 +128,8 @@ impl CodeIndexSchedulerRegistryV1 { #[cfg(test)] pub fn install_cold_mount_post_check_gate(&self, project_root: &Path) { - let project_root = project_root - .canonicalize() - .expect("canonical test project root"); + let project_root = + canonical_existing_identity(project_root).expect("canonical test project root"); let replaced = cold_mount_post_check_controls() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) @@ -150,9 +149,8 @@ impl CodeIndexSchedulerRegistryV1 { #[cfg(test)] pub async fn wait_for_cold_mount_post_check(&self, project_root: &Path) { - let project_root = project_root - .canonicalize() - .expect("canonical test project root"); + let project_root = + canonical_existing_identity(project_root).expect("canonical test project root"); let control = cold_mount_post_check_controls() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) @@ -167,9 +165,8 @@ impl CodeIndexSchedulerRegistryV1 { #[cfg(test)] pub fn release_cold_mount_post_check(&self, project_root: &Path) { - let project_root = project_root - .canonicalize() - .expect("canonical test project root"); + let project_root = + canonical_existing_identity(project_root).expect("canonical test project root"); cold_mount_post_check_controls() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) @@ -191,9 +188,8 @@ impl CodeIndexSchedulerRegistryV1 { #[cfg(test)] fn install_cold_mount_open_control(project_root: &Path, blocks_open: bool) { - let project_root = project_root - .canonicalize() - .expect("canonical test project root"); + let project_root = + canonical_existing_identity(project_root).expect("canonical test project root"); let replaced = cold_mount_open_controls() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) @@ -265,7 +261,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; self.cold_mount_reservations .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) @@ -359,7 +355,7 @@ impl CodeIndexSchedulerRegistryV1 { fn cold_mount_open_control_for_test( project_root: &Path, ) -> Option> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; cold_mount_open_controls() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) @@ -528,7 +524,7 @@ impl CodeIndexSchedulerRegistryV1 { /// `reconcile_in_progress_for_test`. #[cfg(test)] pub(crate) async fn pending_wake_micros_for_root(&self, project_root: &Path) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; mounted.get(&project_root).map(|worktree| { worktree @@ -547,7 +543,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option>>> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; mounted .get(&project_root) @@ -562,7 +558,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option>> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; mounted .get(&project_root) @@ -576,7 +572,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; mounted .get(&project_root) @@ -669,7 +665,7 @@ impl CodeIndexSchedulerRegistryV1 { &self, project_root: &Path, ) -> Option>> { - let project_root = project_root.canonicalize().ok()?; + let project_root = canonical_existing_identity(project_root).ok()?; let mounted = self.mounted.lock().await; mounted .get(&project_root) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs index 4f1c80d490..e33b11a62d 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs @@ -35,6 +35,7 @@ use crate::code_index_scheduler::{ CodeIndexHintPolicyV1, CodeIndexReconcileOutcomeV1, CodeIndexSchedulerRegistryV1, CodeIndexWorktreeSchedulerV1, SharedCodeIndexBytePoolV1, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[cfg(feature = "hotpath-alloc")] #[global_allocator] @@ -1239,7 +1240,7 @@ async fn wait_for_settled_owner(registry: &CodeIndexSchedulerRegistryV1, path: & /// admission, so a caller that then seats a crafted owner cannot lose to a /// worker tail that was still owed a pass. async fn drain_clone_backfill(registry: &CodeIndexSchedulerRegistryV1, path: &Path) { - let canonical = path.canonicalize().expect("canonical project"); + let canonical = canonical_existing_identity(path).expect("canonical project"); let deadline = Instant::now() + SERVING_SEAT_FAILURE_CEILING; loop { assert!( diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs index 8b9b1e5aed..25d49aa0a0 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs @@ -60,6 +60,7 @@ use crate::{ }, }, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[test] fn one_file_increment_captures_only_edited_bytes_with_one_thousand_unchanged_files() { @@ -635,7 +636,7 @@ async fn registry_feeds_publications_and_bounded_freshness_reads() { .expect("initial publication event"); assert_eq!( initial.project_root, - fixture.path().canonicalize().expect("canonical fixture") + canonical_existing_identity(fixture.path()).expect("canonical fixture") ); // Publication is not the seated dashboard identity. Wait for the seat // before asserting the projected generation id. @@ -892,10 +893,7 @@ async fn restart_remount_seats_the_retained_generation_before_a_dirty_rebuild() fixture.edit("src/lib.rs", "pub fn alpha() -> u32 { 2 }\n"); let restarted = CodeIndexSchedulerRegistryV1::new(1); - let remount_root = fixture - .path() - .canonicalize() - .expect("canonical remount root"); + let remount_root = canonical_existing_identity(fixture.path()).expect("canonical remount root"); let (recovery_entered, release_successor) = restarted .pause_next_retained_graph_recovery_before_successor(remount_root.clone()) .await; @@ -1651,7 +1649,7 @@ async fn paused_cold_mount_rejects_a_root_retiring_before_final_commit() { let fixture = GitFixture::new(&[("src/main.rs", "fn main() {}\n")]); let store = TempDir::new().expect("store root"); let registry = CodeIndexSchedulerRegistryV1::new(2); - let root = fixture.path().canonicalize().expect("canonical root"); + let root = canonical_existing_identity(fixture.path()).expect("canonical root"); let (cold_commit_entered, release_cold_commit) = registry .pause_next_cold_mount_before_final_commit(root.clone()) .await; @@ -2121,7 +2119,7 @@ async fn unchanged_git_watcher_probe_does_not_enqueue_authoritative_capture() { .await .expect("mount graph-off retained generation"); - let canonical_root = fixture.path().canonicalize().expect("canonical fixture"); + let canonical_root = canonical_existing_identity(fixture.path()).expect("canonical fixture"); let scheduler = { let mounted = registry.mounted.lock().await; Arc::clone( @@ -2233,7 +2231,7 @@ async fn proven_seated_generation_serves_verified_reads_while_reconcile_owns_the let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -2346,7 +2344,7 @@ async fn busy_scheduler_still_refuses_a_seated_generation_without_a_currency_wit let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -2442,7 +2440,7 @@ async fn unchanged_pass_binds_its_source_proof_to_an_unproven_seat() { let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .serving_generation, ) @@ -2642,7 +2640,7 @@ async fn long_text_projection_renews_source_before_seating_and_noop_follow_up_se let fixture = GitFixture::new(ALPHA_LIB_V1); let store = TempDir::new().expect("store root"); let registry = CodeIndexSchedulerRegistryV1::with_background_reconcile_permits(1, 1); - let canonical_root = fixture.path().canonicalize().expect("canonical fixture"); + let canonical_root = canonical_existing_identity(fixture.path()).expect("canonical fixture"); let (projection_started, release_projection) = registry .pause_next_published_text_projection(canonical_root) .await; @@ -2843,7 +2841,7 @@ async fn background_worker_waits_for_global_admission_before_publication_gate() let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .build_publication_lock, ) @@ -2915,7 +2913,7 @@ async fn ignored_dependency_waits_for_global_admission_before_publication_gate() let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .build_publication_lock, ) @@ -3102,10 +3100,7 @@ async fn a_same_content_successor_pointer_keeps_the_seated_generation_serving() advance_pointer_to_unseated_successor( &super::super::scoped_code_index_store_root( store.path(), - &fixture - .path() - .canonicalize() - .expect("canonical fixture root"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture root"), ), false, ); @@ -3159,10 +3154,7 @@ async fn a_different_content_successor_pointer_refuses_the_stale_seat() { advance_pointer_to_unseated_successor( &super::super::scoped_code_index_store_root( store.path(), - &fixture - .path() - .canonicalize() - .expect("canonical fixture root"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture root"), ), true, ); @@ -3333,7 +3325,7 @@ async fn first_activation_conflict_retries_once_and_then_seats() { let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, worktree_id, sealed_generation_id) = { let mut scheduler = scheduler( @@ -3722,10 +3714,8 @@ async fn dashboard_progress_does_not_wait_for_the_scheduler_mutex() { drain_clone_backfill(®istry, fixture.path()).await; settled_owner_with_idle_admission(®istry, fixture.path()).await; let _quiet_owner = quiesced_background_reconcile_admission(®istry, fixture.path()).await; - let canonical_root = fixture - .path() - .canonicalize() - .expect("canonical fixture root"); + let canonical_root = + canonical_existing_identity(fixture.path()).expect("canonical fixture root"); let (scheduler, progress_slot, scope) = { let mounted = registry.mounted.lock().await; let worktree = mounted.get(&canonical_root).expect("mounted worktree"); @@ -3815,10 +3805,8 @@ async fn busy_query_does_not_rearm_dashboard_verification() { drain_clone_backfill(®istry, fixture.path()).await; settled_owner_with_idle_admission(®istry, fixture.path()).await; let admission = quiesced_background_reconcile_admission(®istry, fixture.path()).await; - let canonical_root = fixture - .path() - .canonicalize() - .expect("canonical fixture root"); + let canonical_root = + canonical_existing_identity(fixture.path()).expect("canonical fixture root"); let scope = { let mounted = registry.mounted.lock().await; let worktree = mounted.get(&canonical_root).expect("mounted worktree"); @@ -3947,10 +3935,8 @@ async fn replay_binding_does_not_wait_for_the_scheduler_mutex() { .await .expect("mount daemon-owned scheduler"); let generation_id = wait_for_initial_generation(®istry, fixture.path()).await; - let canonical_root = fixture - .path() - .canonicalize() - .expect("canonical fixture root"); + let canonical_root = + canonical_existing_identity(fixture.path()).expect("canonical fixture root"); let scheduler = { let mounted = registry.mounted.lock().await; Arc::clone( @@ -4020,7 +4006,7 @@ async fn unchanged_background_freshness_probe_posts_no_overflow_wake() { drain_clone_backfill(®istry, fixture.path()).await; wait_for_settled_owner(®istry, fixture.path()).await; wait_for_event_to_ready(®istry).await; - let canonical = fixture.path().canonicalize().expect("canonical fixture"); + let canonical = canonical_existing_identity(fixture.path()).expect("canonical fixture"); { let mounted = registry.mounted.lock().await; let scheduler = &mounted.get(&canonical).expect("mounted worktree").scheduler; @@ -4201,7 +4187,7 @@ async fn elapsed_freshness_window_alone_does_not_make_dashboard_state_stale() { drain_clone_backfill(®istry, fixture.path()).await; settled_owner_with_idle_admission(®istry, fixture.path()).await; let _quiet_owner = quiesced_background_reconcile_admission(®istry, fixture.path()).await; - let canonical = fixture.path().canonicalize().expect("canonical fixture"); + let canonical = canonical_existing_identity(fixture.path()).expect("canonical fixture"); let scope = { let mounted = registry.mounted.lock().await; let worktree = mounted.get(&canonical).expect("mounted worktree"); @@ -5355,7 +5341,7 @@ async fn project_retirement_retains_blocked_worker_owner_until_retry_joins_it() }) .await .expect("worker admits a reconcile pass before retirement"); - let roots = [fixture.path().canonicalize().expect("canonical root")] + let roots = [canonical_existing_identity(fixture.path()).expect("canonical root")] .into_iter() .collect(); @@ -5449,7 +5435,7 @@ async fn concurrent_query_admissions_claim_one_pending_wake_before_worker_coales let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -5896,7 +5882,8 @@ async fn retirement_waits_for_and_fences_an_exact_cold_mount_open() { let mut cancelled = registry .subscribe_cold_mount_cancellation(fixture.path()) .expect("cold mount reservation"); - let roots = BTreeSet::from([fixture.path().canonicalize().expect("canonical root")]); + let roots = + BTreeSet::from([canonical_existing_identity(fixture.path()).expect("canonical root")]); let retirement = { let registry = registry.clone(); tokio::spawn(async move { @@ -7759,7 +7746,7 @@ async fn mount_with_retained_generation_verifies_cadence_promptly() { let bytes = Arc::new(SharedCodeIndexBytePoolV1::default()); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let first_generation = { let mut scheduler = scheduler(&fixture, scoped_store, Arc::clone(&bytes)); @@ -7824,7 +7811,7 @@ async fn mount_verification_noop_emits_event_to_ready_receipt() { let bytes = Arc::new(SharedCodeIndexBytePoolV1::default()); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); { let mut scheduler = scheduler(&fixture, scoped_store.clone(), Arc::clone(&bytes)); @@ -7903,7 +7890,7 @@ async fn witness_verified_mount_activates_without_rebuild() { let bytes = Arc::new(SharedCodeIndexBytePoolV1::default()); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let seeded = { let mut scheduler = scheduler(&fixture, scoped_store.clone(), Arc::clone(&bytes)); @@ -7946,7 +7933,7 @@ async fn reopened_current_text_generation_resolves_publication_identity_without_ let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (generation, scope) = { let mut scheduler = scheduler( @@ -8048,7 +8035,7 @@ async fn failed_retained_activation_never_installs_unverified_serving_state() { let bytes = Arc::new(SharedCodeIndexBytePoolV1::default()); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let scope = { let mut scheduler = scheduler(&fixture, scoped_store, bytes); @@ -8086,7 +8073,7 @@ async fn failed_retained_activation_never_installs_unverified_serving_state() { let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -8197,7 +8184,7 @@ async fn resident_memory_graph_refusal_seats_text_serving_without_graph() { let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, worktree_id) = { let mut scheduler = scheduler( @@ -8763,7 +8750,7 @@ async fn graph_off_changed_source_advances_text_authority_without_full_decode() let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, privacy_domain) = { let mut scheduler = scheduler( @@ -8804,7 +8791,7 @@ async fn graph_off_changed_source_advances_text_authority_without_full_decode() let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -9167,7 +9154,7 @@ async fn pinned_configuration_refuses_native_graph_before_text_serving_swap() { let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let scope = { let mut scheduler = scheduler( @@ -9339,7 +9326,7 @@ async fn same_root_remount_updates_retained_graph_policy_before_worker_activatio let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let scope = { let mut scheduler = scheduler( @@ -9430,7 +9417,7 @@ async fn graph_off_remount_preserves_an_unhinted_source_reconcile() { let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, generation_a) = { let mut scheduler = scheduler( @@ -9554,7 +9541,7 @@ async fn retryable_graph_activation_does_not_block_changed_text_generation() { let bytes = Arc::new(SharedCodeIndexBytePoolV1::default()); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, sealed_worktree_id, sealed_generation_id) = { let mut scheduler = scheduler(&fixture, scoped_store.clone(), bytes); @@ -9807,7 +9794,7 @@ fn dashboard_graph_readiness_follows_the_current_text_generation() { let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let mut scheduler = scheduler( &fixture, @@ -9846,7 +9833,7 @@ async fn terminal_graph_activation_failure_is_typed_for_current_text_generation( let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, worktree_id) = { let mut scheduler = scheduler( @@ -9946,7 +9933,7 @@ async fn graph_decode_does_not_block_text_freshness() { let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let scope = { let mut scheduler = scheduler( @@ -10400,7 +10387,7 @@ async fn continuously_edited_tree_still_seats_the_sealed_graph_generation() { let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let scope = { let mut scheduler = scheduler( diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/retained_configuration_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/retained_configuration_tests.rs index cb0fc7021b..fa66cfaccf 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/retained_configuration_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/retained_configuration_tests.rs @@ -9,6 +9,7 @@ use super::{ }; use crate::code_index::production::DAEMON_CODE_INDEX_CHUNKER_REVISION; use crate::code_index_scheduler::scoped_code_index_store_root; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn partitioned_restart_rebuilds_incompatible_retained_generation() { @@ -19,7 +20,7 @@ async fn partitioned_restart_rebuilds_incompatible_retained_generation() { let store = TempDir::new().expect("store root"); let scoped_store = scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let retained_generation = { let mut seed = scheduler( @@ -114,7 +115,7 @@ async fn partitioned_restart_rebuilds_incompatible_retained_generation() { !current_status.rebuild_in_flight, "status must clear rebuild liveness after the replacement becomes current" ); - let canonical_root = fixture.path().canonicalize().expect("canonical fixture"); + let canonical_root = canonical_existing_identity(fixture.path()).expect("canonical fixture"); let scheduler = { let mounted = registry.mounted.lock().await; Arc::clone( diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs index a0ed7e0b3f..1dc5ec30a1 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/serving.rs @@ -72,6 +72,7 @@ use crate::{ CodeIndexReconcileAdmissionV1, CodeIndexSchedulerRegistryV1, SharedCodeIndexBytePoolV1, }, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[test] fn text_artifact_source_batches_scale_with_build_memory() { @@ -1162,7 +1163,7 @@ async fn a_proven_seat_serves_v14_without_asking_for_the_pending_clone_successor { let mounted = registry.mounted.lock().await; let worktree = mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree"); // Generation identity binds the capture instant (`captured_at` is in // the intake digest), so the crafted owner and the registry's own @@ -1272,7 +1273,7 @@ async fn expired_source_proof_reschedules_pending_clone_backfill() { { let mounted = registry.mounted.lock().await; let worktree = mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree"); // Seat the crafted owner alongside its text handle: the worker's // clone-backfill gate only drives a text owner that is the seated @@ -4082,7 +4083,7 @@ async fn search_serves_the_last_complete_generation_while_the_scheduler_rebuilds let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -4200,7 +4201,7 @@ async fn search_never_awaits_an_in_flight_decode_while_a_generation_is_servable( let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -4282,7 +4283,7 @@ async fn search_refusal_with_nothing_servable_never_joins_the_decode() { let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -4342,7 +4343,7 @@ async fn root_graph_ready_does_not_depend_on_the_publication_decode_cache() { let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -4626,7 +4627,7 @@ async fn search_requests_one_background_reconcile_when_nothing_is_servable() { let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) @@ -5408,7 +5409,7 @@ async fn callable_application_operations_consume_exact_lexical_and_graph_owners( let warming_text = { let mounted = registry.mounted.lock().await; mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .historical_generation_owner .published_text_generation(&generation) @@ -5431,7 +5432,7 @@ async fn callable_application_operations_consume_exact_lexical_and_graph_owners( let scheduler = { let mounted = registry.mounted.lock().await; let worktree = mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree"); *worktree .serving_generation @@ -5888,8 +5889,9 @@ async fn graph_cursor_holds_its_generation_until_the_cursor_expires() { async fn unpinned_query_resolves_exact_admitted_worktree_scope() { let left = GitFixture::new(&[("src/lib.rs", "pub fn left_only() {}\n")]); let right = GitFixture::new(&[("src/lib.rs", "pub fn right_only() {}\n")]); - let (first, target, target_literal) = if left.path().canonicalize().expect("left root") - < right.path().canonicalize().expect("right root") + let (first, target, target_literal) = if canonical_existing_identity(left.path()) + .expect("left root") + < canonical_existing_identity(right.path()).expect("right root") { (&left, &right, "right_only") } else { @@ -6540,7 +6542,7 @@ async fn graph_off_overflow_preserves_text_owner_progress_without_full_decode() let store = TempDir::new().expect("store root"); let scoped_store = super::super::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, privacy_domain) = { let mut scheduler = scheduler( @@ -6588,7 +6590,7 @@ async fn graph_off_overflow_preserves_text_owner_progress_without_full_decode() let mounted = registry.mounted.lock().await; Arc::clone( &mounted - .get(&fixture.path().canonicalize().expect("canonical root")) + .get(&canonical_existing_identity(fixture.path()).expect("canonical root")) .expect("mounted worktree") .scheduler, ) diff --git a/crates/tracedecay-code-index-runtime/src/git_watch.rs b/crates/tracedecay-code-index-runtime/src/git_watch.rs index 306ccfceee..a1d2986574 100644 --- a/crates/tracedecay-code-index-runtime/src/git_watch.rs +++ b/crates/tracedecay-code-index-runtime/src/git_watch.rs @@ -77,6 +77,7 @@ use ownership::{GitWatcherTaskFailure, GitWatcherTaskFailureKind, GitWatcherTask #[cfg(test)] use state::WorktreeRegistration; use state::{WatchCancellation, WatchState}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; #[cfg(test)] use watch_plan::{MAX_METADATA_WATCH_DIRECTORIES, observe_watch_plan}; use watch_plan::{WatchInstallFailure, WatchPlanFailure, install_watches}; @@ -398,8 +399,7 @@ impl GitWatcher { "reason": "project_path_missing", }); }; - let canonical = project_root - .canonicalize() + let canonical = canonical_existing_identity(project_root) .unwrap_or_else(|_| project_root.to_path_buf()); let state = { let projects = self.inner.projects.lock().await; diff --git a/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission.rs b/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission.rs index 35e62cfe48..977590e73e 100644 --- a/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission.rs +++ b/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission.rs @@ -15,6 +15,7 @@ use crate::code_index_scheduler::{ CodeIndexIgnoredDependencyRefusalV1, CodeIndexIgnoredDependencyRequestV1, CodeIndexSchedulerErrorV1, CodeIndexSchedulerRegistryV1, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; struct ProjectCodeIndexIgnoredDependencyAdmissionPortV1 { schedulers: CodeIndexSchedulerRegistryV1, @@ -74,11 +75,12 @@ impl CodeIndexIgnoredDependencyAdmissionPortV1 if !self.database_writable { return Err(CodeIndexIgnoredDependencyAdmissionErrorV1::ReadOnly); } - let project_root = self.project_root.canonicalize().map_err(|error| { - CodeIndexIgnoredDependencyAdmissionErrorV1::Unavailable { - detail: format!("ignored-dependency project root is unavailable: {error}"), - } - })?; + let project_root = + canonical_existing_identity(&self.project_root).map_err(|error| { + CodeIndexIgnoredDependencyAdmissionErrorV1::Unavailable { + detail: format!("ignored-dependency project root is unavailable: {error}"), + } + })?; let scheduler_request = CodeIndexIgnoredDependencyRequestV1 { scope: self.scope.clone(), expected_generation: request.source_generation().clone(), diff --git a/crates/tracedecay-code-index-runtime/src/project_reads/scope_admission_tests.rs b/crates/tracedecay-code-index-runtime/src/project_reads/scope_admission_tests.rs index cc44602284..aa4eb9b589 100644 --- a/crates/tracedecay-code-index-runtime/src/project_reads/scope_admission_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/project_reads/scope_admission_tests.rs @@ -23,6 +23,7 @@ use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; use super::project_code_graph_projection_read_port; use crate::code_index_scheduler::{CodeIndexSchedulerRegistryV1, LatestCompleteCodeIndexV1}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; const PROJECT_ID: &str = "project.project-open-code-graph-scope"; @@ -316,7 +317,7 @@ async fn wait_for_initial_generation(registry: &CodeIndexSchedulerRegistryV1, pr if registry.latest_generation_id(project_root).await.is_some() { return; } - let canonical_root = project_root.canonicalize().expect("canonical fixture root"); + let canonical_root = canonical_existing_identity(project_root).expect("canonical fixture root"); let mut publications = registry.subscribe_generation_publications(); tokio::time::timeout(Duration::from_secs(5), async { loop { diff --git a/crates/tracedecay-global-db/src/project_registry.rs b/crates/tracedecay-global-db/src/project_registry.rs index 8e8b348332..fe88180d9c 100644 --- a/crates/tracedecay-global-db/src/project_registry.rs +++ b/crates/tracedecay-global-db/src/project_registry.rs @@ -282,7 +282,7 @@ impl ProjectIdentityAliasKind { /// the keys written while the path existed, so a moved project stays /// resolvable by its former root. pub(super) fn canonical_project_path(project_path: &Path) -> PathBuf { - tracedecay_runtime_core::path_safety::canonicalize_path_or_existing_parent(project_path) + tracedecay_runtime_core::path_safety::canonical_root_identity(project_path) } pub(super) fn project_path_alias_key(project_path: &Path) -> String { diff --git a/crates/tracedecay-runtime-core/src/path_safety.rs b/crates/tracedecay-runtime-core/src/path_safety.rs index b99343a41f..96aeedbee5 100644 --- a/crates/tracedecay-runtime-core/src/path_safety.rs +++ b/crates/tracedecay-runtime-core/src/path_safety.rs @@ -94,6 +94,14 @@ pub fn canonical_root_identity(path: &Path) -> PathBuf { plain_host_path(&canonicalize_path_or_existing_parent(path)) } +/// [`canonical_root_identity`] for a path that must exist: a missing path is +/// the [`std::fs::canonicalize`] error instead of a name resolved through its +/// deepest existing ancestor. +pub fn canonical_existing_identity(path: &Path) -> io::Result { + path.canonicalize() + .map(|canonical| plain_host_path(&canonical)) +} + /// Rewrites a Windows extended-length (`\\?\`) *disk* path to its ordinary /// form, so it can be handed to a tool that does not understand the verbatim /// prefix. diff --git a/crates/tracedecay/src/daemon/code_index_runtime_generation_census_tests.rs b/crates/tracedecay/src/daemon/code_index_runtime_generation_census_tests.rs index 8a4ceae8ac..89293482da 100644 --- a/crates/tracedecay/src/daemon/code_index_runtime_generation_census_tests.rs +++ b/crates/tracedecay/src/daemon/code_index_runtime_generation_census_tests.rs @@ -15,6 +15,7 @@ use tracedecay_code_index_runtime::code_index_scheduler::CodeIndexSchedulerRegis use tracedecay_code_index_runtime::project_reads::project_code_index_generation_census_reader; use tracedecay_code_index_runtime::resolved_scope_for_project; use tracedecay_runtime_core::config::PinnedUserDataDir; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; use tracedecay_runtime_core::runtime_telemetry::{ GenerationCensusSnapshot, GenerationCensusUnavailableReason, }; @@ -49,7 +50,8 @@ async fn runtime_mcp_refuses_counts_until_the_mounted_graph_can_serve_queries() project_code_index_generation_census_reader(schedulers.clone(), project.clone(), scope); if schedulers.latest_generation_id(&project).await.is_none() { - let canonical_project = project.canonicalize().expect("canonical fixture root"); + let canonical_project = + canonical_existing_identity(&project).expect("canonical fixture root"); tokio::time::timeout(std::time::Duration::from_secs(5), async { loop { let publication = publications.recv().await.expect("generation publication"); diff --git a/crates/tracedecay/src/daemon/code_index_runtime_graph_activation_tests.rs b/crates/tracedecay/src/daemon/code_index_runtime_graph_activation_tests.rs index 00d430ebac..1e7dc739dc 100644 --- a/crates/tracedecay/src/daemon/code_index_runtime_graph_activation_tests.rs +++ b/crates/tracedecay/src/daemon/code_index_runtime_graph_activation_tests.rs @@ -36,6 +36,7 @@ use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; use tracedecay_code_index_runtime::project_reads::{ project_code_graph_projection_read_port, project_code_index_generation_census_reader, }; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; const ALPHA_LIB_V1: &[(&str, &str)] = &[("src/lib.rs", "pub fn alpha() -> u32 { 1 }\n")]; @@ -159,7 +160,7 @@ async fn failed_cold_mount_graph_replay_preserves_retained_text_generation() { let bytes = Arc::new(SharedCodeIndexBytePoolV1::default()); let scoped_store = scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (scope, seeded_generation_id) = { let mut scheduler = scheduler(&fixture, scoped_store, bytes); @@ -316,7 +317,7 @@ async fn persistent_graph_activation_publishes_a_small_generation() { let store = TempDir::new().expect("store root"); let scoped_store = scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (latest, replay_binding, repository_id, worktree_id) = { let mut scheduler = scheduler( @@ -438,7 +439,7 @@ async fn persistent_callers_cursor_keeps_generation_a_without_repointing_generat let store = TempDir::new().expect("store root"); let scoped_store = scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let (latest_a, replay_a, scope, hub) = { let mut scheduler = scheduler( @@ -723,7 +724,7 @@ async fn restart_status_case(corrupt_graph: bool, dirty_before_restart: bool) { let store = TempDir::new().expect("store root"); let scoped_store = scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let ( scope, @@ -889,7 +890,7 @@ async fn restart_status_case(corrupt_graph: bool, dirty_before_restart: bool) { Some( registry .pause_next_retained_graph_recovery_before_successor( - fixture.path().canonicalize().expect("canonical fixture"), + canonical_existing_identity(fixture.path()).expect("canonical fixture"), ) .await, ) @@ -1245,7 +1246,7 @@ async fn restart_seats_the_retained_graph_while_its_text_owner_still_projects() use tracedecay_application::lsp_runtime::LspCodeIndexProjectionIdentityPort; let fixture = GitFixture::new(ALPHA_LIB_V1); - let canonical_fixture = fixture.path().canonicalize().expect("canonical fixture"); + let canonical_fixture = canonical_existing_identity(fixture.path()).expect("canonical fixture"); let store = TempDir::new().expect("store root"); let scoped_store = scoped_code_index_store_root(store.path(), &canonical_fixture); let (scope, seeded_generation_id, latest, replay_binding, repository_id, worktree_id) = { diff --git a/crates/tracedecay/src/daemon/production_harness.rs b/crates/tracedecay/src/daemon/production_harness.rs index 39bd85c3cc..dd65b74e08 100644 --- a/crates/tracedecay/src/daemon/production_harness.rs +++ b/crates/tracedecay/src/daemon/production_harness.rs @@ -773,16 +773,15 @@ impl ProductionProjectCompositionHarnessV1 { pub fn server(&self, project_root: impl AsRef) -> Result> { let canonical_project_path = - std::fs::canonicalize(project_root.as_ref()).map_err(|error| { - TraceDecayError::Config { - message: format!( - "failed to canonicalize production-composition project '{}': {error}", - project_root.as_ref().display() - ), - } + tracedecay_runtime_core::path_safety::canonical_existing_identity( + project_root.as_ref(), + ) + .map_err(|error| TraceDecayError::Config { + message: format!( + "failed to canonicalize production-composition project '{}': {error}", + project_root.as_ref().display() + ), })?; - let canonical_project_path = - tracedecay_runtime_core::path_safety::plain_host_path(&canonical_project_path); self.resources .as_ref() .and_then(|resources| resources.servers.get(&canonical_project_path)) diff --git a/crates/tracedecay/src/daemon/production_harness/generation_retention_test.rs b/crates/tracedecay/src/daemon/production_harness/generation_retention_test.rs index d2b47bd5f7..002911a44e 100644 --- a/crates/tracedecay/src/daemon/production_harness/generation_retention_test.rs +++ b/crates/tracedecay/src/daemon/production_harness/generation_retention_test.rs @@ -15,6 +15,7 @@ use tracedecay_code_index_retention::code_index_generations::{ prepare_next_code_generation_retention_cancellable, }; use tracedecay_maintenance::tick::{MaintenanceContinuation, MaintenanceTickOutcome}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; fn initialize_git_project(root: &Path) { git(root, &["init", "-q", "-b", "main"]); @@ -101,10 +102,8 @@ async fn mounted_code_generation_retention_continues_capped_segment_reclamation( // The scheduler hashes the canonical project root. A non-canonical // `project_root()` names a store that is never created, and // `latest_generation_id` still answers because it canonicalizes itself. - let canonical_root = graph - .project_root() - .canonicalize() - .expect("canonical project root"); + let canonical_root = + canonical_existing_identity(graph.project_root()).expect("canonical project root"); let first_source = schedulers .latest_generation_id(&canonical_root) .await diff --git a/crates/tracedecay/src/daemon/project_composition/code_index_activation.rs b/crates/tracedecay/src/daemon/project_composition/code_index_activation.rs index 09c6971d58..8b58f4c51c 100644 --- a/crates/tracedecay/src/daemon/project_composition/code_index_activation.rs +++ b/crates/tracedecay/src/daemon/project_composition/code_index_activation.rs @@ -392,6 +392,7 @@ mod tests { use super::*; use tempfile::TempDir; + use tracedecay_runtime_core::path_safety::canonical_existing_identity; fn git(root: &Path, arguments: &[&str]) { let status = Command::new( @@ -508,10 +509,8 @@ mod tests { #[tokio::test] async fn reconcile_request_before_mount_activates_indexing() { let repository = repository(); - let root = repository - .path() - .canonicalize() - .expect("canonical repository root"); + let root = + canonical_existing_identity(repository.path()).expect("canonical repository root"); let mount_attempts = Arc::new(AtomicUsize::new(0)); let mount: code_index_scheduler::CodeIndexActivationMountV1 = { let mount_attempts = Arc::clone(&mount_attempts); @@ -580,10 +579,8 @@ mod tests { #[tokio::test] async fn explicit_reconcile_overrides_linked_worktree_watch_policy() { let repository = repository(); - let root = repository - .path() - .canonicalize() - .expect("canonical repository root"); + let root = + canonical_existing_identity(repository.path()).expect("canonical repository root"); let mount_attempts = Arc::new(AtomicUsize::new(0)); let mount: code_index_scheduler::CodeIndexActivationMountV1 = { let mount_attempts = Arc::clone(&mount_attempts); diff --git a/crates/tracedecay/src/daemon/project_open_admission.rs b/crates/tracedecay/src/daemon/project_open_admission.rs index 2d9b4b8199..f487318c07 100644 --- a/crates/tracedecay/src/daemon/project_open_admission.rs +++ b/crates/tracedecay/src/daemon/project_open_admission.rs @@ -1194,8 +1194,8 @@ impl ProjectRouteKey { global_db_path: authority::canonical_identity_path( &handshake.client_identity.global_db_path, )?, - project_path: tracedecay_runtime_core::path_safety::plain_host_path( - &authority::canonical_identity_path(project_path)?, + project_path: tracedecay_runtime_core::path_safety::canonical_root_identity( + project_path, ), scope_prefix: handshake.scope_prefix.clone(), }) diff --git a/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs b/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs index 2b72305215..144b3b2f71 100644 --- a/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs +++ b/crates/tracedecay/src/daemon/project_open_owners/code_index_reads/ignored_dependency_admission_tests.rs @@ -23,6 +23,7 @@ use tracedecay_code_index_runtime::code_index_scheduler::{ CodeGraphActivationPolicyV1, CodeIndexSchedulerRegistryV1, LatestCompleteCodeIndexV1, }; use tracedecay_code_index_runtime::project_reads::project_code_index_ignored_dependency_admission_port; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; const PROJECT_ID: &str = "project.project-open-ignored-dependency"; @@ -360,7 +361,7 @@ async fn wait_for_initial_generation(registry: &CodeIndexSchedulerRegistryV1, pr if registry.latest_generation_id(project_root).await.is_some() { return; } - let canonical_root = project_root.canonicalize().expect("canonical fixture root"); + let canonical_root = canonical_existing_identity(project_root).expect("canonical fixture root"); let mut publications = registry.subscribe_generation_publications(); tokio::time::timeout(Duration::from_secs(5), async { loop { diff --git a/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/cancellation_tests.rs b/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/cancellation_tests.rs index 22c44b1236..9303613421 100644 --- a/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/cancellation_tests.rs +++ b/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/cancellation_tests.rs @@ -5,6 +5,7 @@ use tracedecay_code_index::production::{ }; use super::{CodeIndexSchedulerErrorV1, GitFixture}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; /// The shared source readers run under the ordinary reconcile as well as under /// an ignored-dependency admission, so they report the reconcile interruption; @@ -59,10 +60,7 @@ fn admitted_source_read_observes_live_cancellation_between_chunks() { // Cancel on the second read checkpoint, after one full chunk was observed. let control = CancelAfterChecks::new(5); // The scheduler supplies a canonical root; TempDir may retain a system alias. - let project_root = fixture - .path() - .canonicalize() - .expect("canonical fixture root"); + let project_root = canonical_existing_identity(fixture.path()).expect("canonical fixture root"); let error = tracedecay_code_index_runtime::code_index_scheduler::ignored_dependencies::read_bounded_admitted_source( &project_root, diff --git a/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/flight_tests.rs b/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/flight_tests.rs index 3c7d0c778a..1bffb7fdb5 100644 --- a/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/flight_tests.rs +++ b/crates/tracedecay/tests/daemon_suite/code_index_ignored_dependencies_test/flight_tests.rs @@ -7,6 +7,7 @@ use tracedecay_code_index::production::{ use tracedecay_code_index_retention::code_index_generations::try_acquire_code_generation_store_lock; use super::*; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; struct BlockingNthControl { checks: AtomicUsize, @@ -320,7 +321,7 @@ async fn coalesced_publication_failure_preserves_the_scheduler_error_family() { let scoped_store = tracedecay_code_index_runtime::code_index_scheduler::scoped_code_index_store_root( store.path(), - &fixture.path().canonicalize().expect("canonical fixture"), + &canonical_existing_identity(fixture.path()).expect("canonical fixture"), ); let pointer_path = scoped_store.join("active-code-generation-v1.json"); // Writers rename a temporary over the active pointer while holding the diff --git a/crates/tracedecay/tests/daemon_suite/code_index_journey.rs b/crates/tracedecay/tests/daemon_suite/code_index_journey.rs index b388b9d082..de9c031660 100644 --- a/crates/tracedecay/tests/daemon_suite/code_index_journey.rs +++ b/crates/tracedecay/tests/daemon_suite/code_index_journey.rs @@ -19,6 +19,7 @@ use tracedecay_daemon_protocol::DaemonHandshake; use tracedecay_hooks::core_events::{DaemonHookEvent, HookAgent, HookEventNotifyOutcomeV1}; use crate::common::{DaemonProcess, tracedecay_command_with_home}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; pub const RECEIPT_TIMEOUT: Duration = Duration::from_secs(45); @@ -192,7 +193,8 @@ pub fn initialize_tracedecay(home: &Path, project: &Path) -> String { } pub fn exact_identity(project: &Path, project_id: String) -> ExactIndexIdentity { - let canonical_project = project.canonicalize().expect("canonical fixture project"); + let canonical_project = + canonical_existing_identity(project).expect("canonical fixture project"); let common_dir = tracedecay_runtime_core::worktree::git_common_dir(&canonical_project) .expect("fixture Git common directory"); ExactIndexIdentity { @@ -411,7 +413,8 @@ pub fn assert_exact_identity( expected_reference: &str, expected_revision: Option<&str>, ) { - let canonical_project = project.canonicalize().expect("canonical project receipt"); + let canonical_project = + canonical_existing_identity(project).expect("canonical project receipt"); assert_eq!( status["project_root"].as_str(), canonical_project.to_str(), @@ -460,7 +463,9 @@ pub async fn assert_project_identity( ); assert_eq!( context["project"]["canonical_root"].as_str(), - project.canonicalize().expect("canonical project").to_str(), + canonical_existing_identity(project) + .expect("canonical project") + .to_str(), "terminal receipt crossed project root: {context}" ); } diff --git a/crates/tracedecay/tests/daemon_suite/indexing_lifecycle_test.rs b/crates/tracedecay/tests/daemon_suite/indexing_lifecycle_test.rs index e951563afa..fb6c38a8fe 100644 --- a/crates/tracedecay/tests/daemon_suite/indexing_lifecycle_test.rs +++ b/crates/tracedecay/tests/daemon_suite/indexing_lifecycle_test.rs @@ -28,6 +28,7 @@ use crate::code_index_journey::{ wait_for_terminal_generation, }; use crate::common::{EnvVarGuard, IsolatedEnv, daemon_socket_path, spawn_tracedecay_daemon_with}; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; fn initialize_repository(project: &Path) -> (String, String) { fs::create_dir_all(project.join("src")).expect("fixture source directory"); @@ -302,7 +303,7 @@ fn assert_exact_ignored_dependency_roster(generation: &CodeIndexPublishedGenerat #[tokio::test] async fn ignored_dependency_admission_survives_physical_daemon_restart_without_widening() { let (environment, project) = IsolatedEnv::acquire().await; - let project = project.canonicalize().expect("canonical fixture project"); + let project = canonical_existing_identity(&project).expect("canonical fixture project"); let revision = initialize_ignored_dependency_repository(&project); let socket = daemon_socket_path(environment.home()); let mut daemon = spawn_tracedecay_daemon_with(environment.home(), |_| {}); @@ -418,7 +419,7 @@ async fn ignored_dependency_admission_survives_physical_daemon_restart_without_w #[tokio::test] async fn one_line_append_publishes_fresh_generation_with_carried_clone_bodies() { let (environment, project) = IsolatedEnv::acquire().await; - let project = project.canonicalize().expect("canonical fixture project"); + let project = canonical_existing_identity(&project).expect("canonical fixture project"); fs::create_dir_all(project.join("src")).expect("fixture source directory"); fs::write( project.join("Cargo.toml"), @@ -504,7 +505,7 @@ async fn one_line_append_publishes_fresh_generation_with_carried_clone_bodies() #[tokio::test] async fn mounted_incremental_lifecycle_preserves_only_complete_compatible_generations() { let (environment, project) = IsolatedEnv::acquire().await; - let project = project.canonicalize().expect("canonical fixture project"); + let project = canonical_existing_identity(&project).expect("canonical fixture project"); let (main_revision, feature_revision) = initialize_repository(&project); let socket = daemon_socket_path(environment.home()); let log_path = environment