diff --git a/crates/tracedecay-cli/src/lsp_cmd.rs b/crates/tracedecay-cli/src/lsp_cmd.rs index dbce29db39..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, - root.path().canonicalize().expect("canonical workspace") - ); + 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,11 +628,7 @@ mod tests { assert!(binding.workspace_folders.is_sorted()); assert_eq!( binding.project_root, - first - .path() - .canonicalize() - .unwrap() - .min(second.path().canonicalize().unwrap()) + 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/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/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/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/production_harness.rs b/crates/tracedecay/src/daemon/production_harness.rs index 7912b7cce5..dd65b74e08 100644 --- a/crates/tracedecay/src/daemon/production_harness.rs +++ b/crates/tracedecay/src/daemon/production_harness.rs @@ -773,13 +773,14 @@ 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() + ), })?; self.resources .as_ref() 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 c8cf52cc22..f487318c07 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::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/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/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 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, ); }