agpm_cli/resolver/
transitive_resolver.rs

1//! Transitive dependency resolution for AGPM.
2//!
3//! This module handles the discovery and resolution of transitive dependencies,
4//! building dependency graphs, detecting cycles, and providing high-level
5//! orchestration for the entire transitive resolution process. It processes
6//! dependencies declared within resource files and resolves them in topological order.
7//!
8//! ## Parallel Processing Algorithm
9//!
10//! Transitive dependencies are resolved in parallel batches:
11//! 1. Calculate batch size: min(max(10, CPU cores × 2), remaining queue length)
12//! 2. Extract batch from queue (LIFO order to match serial behavior)
13//! 3. Process batch concurrently using join_all
14//! 4. Repeat until queue empty
15//!
16//! Concurrent safety is ensured via `Arc<DashMap>` for shared state.
17//! Each batch processes dependencies independently, with coordination
18//! happening through the shared DashMap-backed registries.
19
20use std::collections::HashSet;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23use std::sync::atomic::{AtomicUsize, Ordering};
24
25use anyhow::{Context, Result};
26use dashmap::DashMap;
27use futures::future::join_all;
28use tokio::sync::{Mutex, MutexGuard};
29
30use crate::core::ResourceType;
31use crate::lockfile::lockfile_dependency_ref::LockfileDependencyRef;
32use crate::manifest::{DetailedDependency, ResourceDependency};
33use crate::metadata::MetadataExtractor;
34use crate::utils;
35use crate::version::conflict::ConflictDetector;
36use crate::version::constraints::VersionConstraint;
37
38use super::dependency_graph::{DependencyGraph, DependencyNode};
39use super::pattern_expander::generate_dependency_name;
40use super::types::{
41    DependencyKey, TransitiveContext, apply_manifest_override, compute_dependency_variant_hash,
42};
43use super::version_resolver::{PreparedSourceVersion, VersionResolutionService};
44use super::{PatternExpansionService, ResourceFetchingService, is_file_relative_path};
45
46use crate::constants::{batch_operation_timeout, default_lock_timeout};
47
48/// Acquire a tokio Mutex with timeout and diagnostic dump on failure.
49///
50/// This prevents deadlocks from hanging indefinitely by timing out and
51/// dumping lock state for debugging. Uses the test-mode-aware timeout
52/// from constants (8s in test mode, 30s in production).
53async fn acquire_mutex_with_timeout<'a, T>(
54    mutex: &'a Mutex<T>,
55    name: &str,
56) -> Result<MutexGuard<'a, T>> {
57    let timeout = default_lock_timeout();
58    match tokio::time::timeout(timeout, mutex.lock()).await {
59        Ok(guard) => Ok(guard),
60        Err(_) => {
61            eprintln!("[DEADLOCK] Timeout waiting for mutex '{}' after {:?}", name, timeout);
62            anyhow::bail!(
63                "Timeout waiting for Mutex '{}' after {:?} - possible deadlock",
64                name,
65                timeout
66            )
67        }
68    }
69}
70
71/// Check if a version string represents a semver constraint.
72///
73/// Returns `true` for semver types (Exact, Requirement) and `false` for GitRef.
74/// This is used to prefer semver versions over floating refs like "main" when
75/// resolving duplicate transitive dependencies.
76fn is_semver_version(version: Option<&str>) -> bool {
77    match version {
78        Some(v) => VersionConstraint::parse(v).is_ok_and(|c| c.is_semver()),
79        None => false,
80    }
81}
82
83/// Determine if a new dependency should replace an existing one based on version preference.
84///
85/// Semver versions (e.g., "v1.0.0", "^1.0.0") are preferred over git refs (e.g., "main").
86/// This ensures stable, reproducible builds by choosing explicit version constraints
87/// over floating branch refs.
88///
89/// Returns `true` if the new dependency should replace the existing one.
90fn should_replace_existing(existing: &ResourceDependency, new: &ResourceDependency) -> bool {
91    let existing_is_semver = is_semver_version(existing.get_version());
92    let new_is_semver = is_semver_version(new.get_version());
93
94    // Only replace if:
95    // 1. New is semver AND existing is NOT semver (semver wins over git refs)
96    // 2. This handles the case where A depends on C@main and B depends on C@v1.0.0
97    if new_is_semver && !existing_is_semver {
98        tracing::debug!(
99            "Preferring semver '{}' over git ref '{}'",
100            new.get_version().unwrap_or("none"),
101            existing.get_version().unwrap_or("none")
102        );
103        return true;
104    }
105
106    false
107}
108
109/// Container for resolution services to reduce parameter count.
110pub struct ResolutionServices<'a> {
111    /// Service for version resolution and commit SHA lookup
112    pub version_service: &'a VersionResolutionService,
113    /// Service for pattern expansion (glob patterns)
114    pub pattern_service: &'a PatternExpansionService,
115}
116
117/// Parameters for transitive resolution to reduce function argument count.
118pub struct TransitiveResolutionParams<'a> {
119    /// Core resolution context
120    pub ctx: &'a mut TransitiveContext<'a>,
121    /// Core resolution services
122    pub core: &'a super::ResolutionCore,
123    /// Base dependencies to resolve
124    pub base_deps: &'a [(String, ResourceDependency, ResourceType)],
125    /// Whether transitive resolution is enabled
126    pub enable_transitive: bool,
127    /// Pre-prepared source versions for resolution (concurrent)
128    pub prepared_versions: &'a Arc<DashMap<String, PreparedSourceVersion>>,
129    /// Map for pattern aliases (concurrent)
130    pub pattern_alias_map: &'a Arc<DashMap<(ResourceType, String), String>>,
131    /// Resolution services
132    pub services: &'a ResolutionServices<'a>,
133    /// Optional progress tracking
134    pub progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
135}
136
137/// Parameters for processing a transitive dependency specification.
138/// This struct reduces cognitive load by grouping related parameters
139/// and makes the function signature more maintainable.
140struct TransitiveDepProcessingParams<'a> {
141    /// The transitive resolution context
142    ctx: &'a TransitiveContext<'a>,
143    /// The core resolution services
144    core: &'a super::ResolutionCore,
145    /// The parent dependency
146    parent_dep: &'a ResourceDependency,
147    /// The resource type of the dependency
148    dep_resource_type: ResourceType,
149    /// The resource type of the parent
150    parent_resource_type: ResourceType,
151    /// The name of the parent resource
152    parent_name: &'a str,
153    /// The dependency specification
154    dep_spec: &'a crate::manifest::DependencySpec,
155    /// The version resolution service
156    version_service: &'a VersionResolutionService,
157    /// Pre-prepared source versions for resolution (concurrent)
158    prepared_versions: &'a Arc<DashMap<String, PreparedSourceVersion>>,
159}
160
161/// Context for processing a single transitive dependency.
162///
163/// Bundles shared state and context to reduce parameter count from 17 to 1,
164/// making the function more maintainable and easier to understand.
165/// This context groups related parameters into logical sections:
166/// - Input: The specific dependency being processed
167/// - Shared: Concurrent state shared across all parallel workers
168/// - Resolution: Core resolution services and context
169/// - Progress: Optional UI progress tracking
170struct TransitiveProcessingContext<'a> {
171    /// Input data for this specific dependency
172    input: TransitiveInput,
173
174    /// Shared concurrent state for processing
175    shared: TransitiveSharedState<'a>,
176
177    /// Resolution context and services
178    resolution: TransitiveResolutionContext<'a>,
179
180    /// Optional progress tracking
181    progress: Option<Arc<utils::MultiPhaseProgress>>,
182}
183
184/// Input data for processing a single dependency.
185///
186/// Contains the specific dependency information that varies for each
187/// function call: name, dependency spec, resource type, and variant hash.
188#[derive(Debug, Clone)]
189struct TransitiveInput {
190    name: String,
191    dep: ResourceDependency,
192    resource_type: ResourceType,
193    variant_hash: String,
194}
195
196/// Shared concurrent state used during processing.
197///
198/// Type alias for the queue entry tuple to reduce type complexity
199type QueueEntry = (String, ResourceDependency, Option<ResourceType>, String);
200
201/// Contains all the Arc-wrapped and shared state structures that need
202/// to be accessed concurrently by multiple workers processing dependencies in parallel.
203/// These are the data structures that were previously passed as individual parameters.
204///
205/// # Concurrency Design
206///
207/// Uses `tokio::sync::Mutex` for queue and graph to avoid blocking the async runtime.
208/// Uses `AtomicUsize` for queue_len to enable lock-free progress tracking.
209struct TransitiveSharedState<'a> {
210    graph: Arc<tokio::sync::Mutex<DependencyGraph>>,
211    all_deps: Arc<DashMap<DependencyKey, ResourceDependency>>,
212    processed: Arc<DashMap<DependencyKey, ()>>,
213    queue: Arc<tokio::sync::Mutex<Vec<QueueEntry>>>,
214    /// Atomic counter for queue length to avoid lock contention during progress updates.
215    /// Updated whenever items are added to/removed from the queue.
216    queue_len: Arc<AtomicUsize>,
217    pattern_alias_map: Arc<DashMap<(ResourceType, String), String>>,
218    completed_counter: Arc<AtomicUsize>,
219    dependency_map: &'a Arc<DashMap<DependencyKey, Vec<String>>>,
220    custom_names: &'a Arc<DashMap<DependencyKey, String>>,
221    prepared_versions: &'a Arc<DashMap<String, PreparedSourceVersion>>,
222}
223
224/// Resolution context and services.
225///
226/// Bundles the core resolution context, manifest overrides, core services,
227/// and resolution services that are needed for processing transitive dependencies.
228/// These are the context references that were previously passed as individual parameters.
229struct TransitiveResolutionContext<'a> {
230    ctx_base: &'a super::types::ResolutionContext<'a>,
231    manifest_overrides: &'a super::types::ManifestOverrideIndex,
232    core: &'a super::ResolutionCore,
233    services: &'a ResolutionServices<'a>,
234}
235
236/// Process a single transitive dependency specification.
237async fn process_transitive_dependency_spec(
238    params: TransitiveDepProcessingParams<'_>,
239) -> Result<(ResourceDependency, String)> {
240    // Get the canonical path to the parent resource file
241    let parent_file_path = ResourceFetchingService::get_canonical_path(
242        params.core,
243        params.parent_dep,
244        params.version_service,
245    )
246    .await
247    .with_context(|| {
248        format!("Failed to get parent path for transitive dependencies of '{}'", params.parent_name)
249    })?;
250
251    // Resolve the transitive dependency path
252    let trans_canonical =
253        resolve_transitive_path(&parent_file_path, &params.dep_spec.path, params.parent_name)?;
254
255    // Create the transitive dependency
256    let trans_dep = create_transitive_dependency(
257        params.ctx,
258        params.parent_dep,
259        params.dep_resource_type,
260        params.parent_resource_type,
261        params.parent_name,
262        params.dep_spec,
263        &parent_file_path,
264        &trans_canonical,
265        params.prepared_versions,
266    )
267    .await?;
268
269    // Generate a name for the transitive dependency using source context
270    let trans_name = if trans_dep.get_source().is_none() {
271        // Local dependency - use manifest directory as source context
272        // Use trans_dep.get_path() which is already relative to manifest directory
273        // (computed in create_path_only_transitive_dep)
274        let manifest_dir = params
275            .ctx
276            .base
277            .manifest
278            .manifest_dir
279            .as_ref()
280            .ok_or_else(|| anyhow::anyhow!("Manifest directory not available"))?;
281
282        let source_context = crate::resolver::source_context::SourceContext::local(manifest_dir);
283        generate_dependency_name(trans_dep.get_path(), &source_context)
284    } else {
285        // Git dependency - use remote source context
286        let source_name = trans_dep
287            .get_source()
288            .ok_or_else(|| anyhow::anyhow!("Git dependency missing source name"))?;
289        let source_context = crate::resolver::source_context::SourceContext::remote(source_name);
290        generate_dependency_name(trans_dep.get_path(), &source_context)
291    };
292
293    Ok((trans_dep, trans_name))
294}
295
296/// Resolve a transitive dependency path relative to its parent.
297fn resolve_transitive_path(
298    parent_file_path: &Path,
299    dep_path: &str,
300    parent_name: &str,
301) -> Result<PathBuf> {
302    // Check if this is a glob pattern
303    let is_pattern = dep_path.contains('*') || dep_path.contains('?') || dep_path.contains('[');
304
305    if is_pattern {
306        // For patterns, normalize (resolve .. and .) but don't canonicalize
307        let parent_dir = parent_file_path.parent().ok_or_else(|| {
308            anyhow::anyhow!(
309                "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
310                dep_path,
311                parent_name
312            )
313        })?;
314        let resolved = parent_dir.join(dep_path);
315
316        // Preserve the root component when normalizing
317        let mut result = PathBuf::new();
318        for component in resolved.components() {
319            match component {
320                std::path::Component::RootDir => result.push(component),
321                std::path::Component::ParentDir => {
322                    result.pop();
323                }
324                std::path::Component::CurDir => {}
325                _ => result.push(component),
326            }
327        }
328        Ok(result)
329    } else if is_file_relative_path(dep_path) || !dep_path.contains('/') {
330        // File-relative path (starts with ./ or ../) or bare filename
331        // For bare filenames, treat as file-relative by resolving from parent directory
332        let parent_dir = parent_file_path.parent().ok_or_else(|| {
333            anyhow::anyhow!(
334                "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
335                dep_path,
336                parent_name
337            )
338        })?;
339
340        let resolved = parent_dir.join(dep_path);
341        resolved.canonicalize().map_err(|e| {
342            // Create a FileOperationError for canonicalization failures
343            let file_error = crate::core::file_error::FileOperationError::new(
344                crate::core::file_error::FileOperationContext::new(
345                    crate::core::file_error::FileOperation::Canonicalize,
346                    &resolved,
347                    format!("resolving transitive dependency '{}' for '{}'", dep_path, parent_name),
348                    "transitive_resolver::resolve_transitive_path",
349                ),
350                e,
351            );
352            anyhow::Error::from(file_error)
353        })
354    } else {
355        // Repo-relative path
356        resolve_repo_relative_path(parent_file_path, dep_path, parent_name)
357    }
358}
359
360/// Resolve a repository-relative transitive dependency path.
361fn resolve_repo_relative_path(
362    parent_file_path: &Path,
363    dep_path: &str,
364    parent_name: &str,
365) -> Result<PathBuf> {
366    // For Git sources, find the worktree root; for local sources, find the source root
367    let repo_root = parent_file_path
368        .ancestors()
369        .find(|p| {
370            // Git worktrees have a .git file (not directory) pointing to the bare repo
371            // This is more robust than checking for underscores in the directory name
372            let git_path = p.join(".git");
373            git_path.is_file()
374        })
375        .or_else(|| parent_file_path.ancestors().nth(2)) // Fallback for local sources
376        .ok_or_else(|| {
377            anyhow::anyhow!(
378                "Failed to find repository root for transitive dependency '{}'",
379                dep_path
380            )
381        })?;
382
383    let full_path = repo_root.join(dep_path);
384    full_path.canonicalize().with_context(|| {
385        format!(
386            "Failed to resolve repo-relative transitive dependency '{}' for '{}': {} (repo root: {})",
387            dep_path,
388            parent_name,
389            full_path.display(),
390            repo_root.display()
391        )
392    })
393}
394
395/// Create a ResourceDependency for a transitive dependency.
396#[allow(clippy::too_many_arguments)]
397async fn create_transitive_dependency(
398    ctx: &TransitiveContext<'_>,
399    parent_dep: &ResourceDependency,
400    dep_resource_type: ResourceType,
401    parent_resource_type: ResourceType,
402    _parent_name: &str,
403    dep_spec: &crate::manifest::DependencySpec,
404    parent_file_path: &Path,
405    trans_canonical: &Path,
406    prepared_versions: &Arc<DashMap<String, PreparedSourceVersion>>,
407) -> Result<ResourceDependency> {
408    use super::types::{OverrideKey, compute_dependency_variant_hash, normalize_lookup_path};
409
410    // Create the dependency as before
411    let mut dep = if parent_dep.get_source().is_none() {
412        create_path_only_transitive_dep(
413            ctx,
414            parent_dep,
415            dep_resource_type,
416            parent_resource_type,
417            dep_spec,
418            trans_canonical,
419        )?
420    } else {
421        create_git_backed_transitive_dep(
422            ctx,
423            parent_dep,
424            dep_resource_type,
425            parent_resource_type,
426            dep_spec,
427            parent_file_path,
428            trans_canonical,
429            prepared_versions,
430        )
431        .await?
432    };
433
434    // Check for manifest override
435    let normalized_path = normalize_lookup_path(dep.get_path());
436    let source = dep.get_source().map(std::string::ToString::to_string);
437
438    // Determine tool for the dependency
439    let tool = dep
440        .get_tool()
441        .map(str::to_string)
442        .unwrap_or_else(|| ctx.base.manifest.get_default_tool(dep_resource_type));
443
444    let variant_hash = compute_dependency_variant_hash(&dep);
445
446    let override_key = OverrideKey {
447        resource_type: dep_resource_type,
448        normalized_path: normalized_path.clone(),
449        source,
450        tool,
451        variant_hash,
452    };
453
454    // Apply manifest override if found
455    if let Some(override_info) = ctx.manifest_overrides.get(&override_key) {
456        apply_manifest_override(&mut dep, override_info, &normalized_path);
457    }
458
459    Ok(dep)
460}
461
462/// Create a path-only transitive dependency (parent is path-only).
463fn create_path_only_transitive_dep(
464    ctx: &TransitiveContext<'_>,
465    parent_dep: &ResourceDependency,
466    dep_resource_type: ResourceType,
467    parent_resource_type: ResourceType,
468    dep_spec: &crate::manifest::DependencySpec,
469    trans_canonical: &Path,
470) -> Result<ResourceDependency> {
471    let manifest_dir = ctx.base.manifest.manifest_dir.as_ref().ok_or_else(|| {
472        anyhow::anyhow!("Manifest directory not available for path-only transitive dep")
473    })?;
474
475    // Always compute relative path from manifest to target
476    let dep_path_str = match manifest_dir.canonicalize() {
477        Ok(canonical_manifest) => {
478            utils::compute_relative_path(&canonical_manifest, trans_canonical)
479        }
480        Err(e) => {
481            eprintln!(
482                "Warning: Could not canonicalize manifest directory {}: {}. Using non-canonical path.",
483                manifest_dir.display(),
484                e
485            );
486            utils::compute_relative_path(manifest_dir, trans_canonical)
487        }
488    };
489
490    // Determine tool for transitive dependency
491    let trans_tool = determine_transitive_tool(
492        ctx,
493        parent_dep,
494        dep_spec,
495        parent_resource_type,
496        dep_resource_type,
497    );
498
499    Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
500        source: None,
501        path: utils::normalize_path_for_storage(dep_path_str),
502        version: None,
503        branch: None,
504        rev: None,
505        command: None,
506        args: None,
507        target: None,
508        filename: None,
509        dependencies: None,
510        tool: trans_tool,
511        flatten: None,
512        install: dep_spec.install.or(Some(true)),
513        template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
514            ctx.base.manifest,
515            parent_dep,
516        )),
517    })))
518}
519
520/// Create a Git-backed transitive dependency (parent is Git-backed).
521#[allow(clippy::too_many_arguments)]
522async fn create_git_backed_transitive_dep(
523    ctx: &TransitiveContext<'_>,
524    parent_dep: &ResourceDependency,
525    dep_resource_type: ResourceType,
526    parent_resource_type: ResourceType,
527    dep_spec: &crate::manifest::DependencySpec,
528    parent_file_path: &Path,
529    trans_canonical: &Path,
530    _prepared_versions: &Arc<DashMap<String, PreparedSourceVersion>>,
531) -> Result<ResourceDependency> {
532    let source_name = parent_dep
533        .get_source()
534        .ok_or_else(|| anyhow::anyhow!("Expected source for Git-backed dependency"))?;
535    let source_url = ctx
536        .base
537        .source_manager
538        .get_source_url(source_name)
539        .ok_or_else(|| anyhow::anyhow!("Source '{source_name}' not found"))?;
540
541    // Get repo-relative path by stripping the appropriate prefix
542    let repo_relative = if utils::is_local_path(&source_url) {
543        strip_local_source_prefix(&source_url, trans_canonical)?
544    } else {
545        // For remote Git sources, derive the worktree root from the parent file path
546        strip_git_worktree_prefix_from_parent(parent_file_path, trans_canonical)?
547    };
548
549    // Determine tool for transitive dependency
550    let trans_tool = determine_transitive_tool(
551        ctx,
552        parent_dep,
553        dep_spec,
554        parent_resource_type,
555        dep_resource_type,
556    );
557
558    Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
559        source: Some(source_name.to_string()),
560        path: utils::normalize_path_for_storage(repo_relative.to_string_lossy().to_string()),
561        version: dep_spec
562            .version
563            .clone()
564            .or_else(|| parent_dep.get_version().map(|v| v.to_string())),
565        branch: None,
566        rev: None,
567        command: None,
568        args: None,
569        target: None,
570        filename: None,
571        dependencies: None,
572        tool: trans_tool,
573        flatten: None,
574        install: dep_spec.install.or(Some(true)),
575        template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
576            ctx.base.manifest,
577            parent_dep,
578        )),
579    })))
580}
581
582/// Strip the local source prefix from a transitive dependency path.
583fn strip_local_source_prefix(source_url: &str, trans_canonical: &Path) -> Result<PathBuf> {
584    let source_url_path = PathBuf::from(source_url);
585    let source_path = source_url_path.canonicalize().map_err(|e| {
586        let file_error = crate::core::file_error::FileOperationError::new(
587            crate::core::file_error::FileOperationContext::new(
588                crate::core::file_error::FileOperation::Canonicalize,
589                &source_url_path,
590                "canonicalizing local source path for transitive dependency".to_string(),
591                "transitive_resolver::strip_local_source_prefix",
592            ),
593            e,
594        );
595        anyhow::Error::from(file_error)
596    })?;
597
598    // Check if this is a pattern path (contains glob characters)
599    let trans_str = trans_canonical.to_string_lossy();
600    let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
601
602    if is_pattern {
603        // For patterns, canonicalize the directory part while keeping the pattern filename intact
604        let parent_dir = trans_canonical.parent().ok_or_else(|| {
605            anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
606        })?;
607        let filename = trans_canonical.file_name().ok_or_else(|| {
608            anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
609        })?;
610
611        // Canonicalize the directory part
612        let canonical_dir = parent_dir.canonicalize().map_err(|e| {
613            let file_error = crate::core::file_error::FileOperationError::new(
614                crate::core::file_error::FileOperationContext::new(
615                    crate::core::file_error::FileOperation::Canonicalize,
616                    parent_dir,
617                    "canonicalizing pattern directory for local source".to_string(),
618                    "transitive_resolver::strip_local_source_prefix",
619                ),
620                e,
621            );
622            anyhow::Error::from(file_error)
623        })?;
624
625        // Reconstruct the full path with canonical directory and pattern filename
626        let canonical_pattern = canonical_dir.join(filename);
627
628        // Now strip the source prefix
629        canonical_pattern
630            .strip_prefix(&source_path)
631            .with_context(|| {
632                format!(
633                    "Transitive pattern dep outside parent's source: {} not under {}",
634                    canonical_pattern.display(),
635                    source_path.display()
636                )
637            })
638            .map(|p| p.to_path_buf())
639    } else {
640        trans_canonical
641            .strip_prefix(&source_path)
642            .with_context(|| {
643                format!(
644                    "Transitive dep resolved outside parent's source directory: {} not under {}",
645                    trans_canonical.display(),
646                    source_path.display()
647                )
648            })
649            .map(|p| p.to_path_buf())
650    }
651}
652
653/// Strip the Git worktree prefix from a transitive dependency path by deriving
654/// the worktree root from the parent file path.
655fn strip_git_worktree_prefix_from_parent(
656    parent_file_path: &Path,
657    trans_canonical: &Path,
658) -> Result<PathBuf> {
659    // Find the worktree root by looking for a directory with a .git file
660    // Git worktrees have a .git file (not directory) that points to the bare repo
661    // This is more robust than checking for underscores in the directory name
662    let worktree_root = parent_file_path
663        .ancestors()
664        .find(|p| {
665            let git_path = p.join(".git");
666            git_path.is_file()
667        })
668        .ok_or_else(|| {
669            anyhow::anyhow!(
670                "Failed to find worktree root from parent file: {}",
671                parent_file_path.display()
672            )
673        })?;
674
675    // Canonicalize worktree root to handle symlinks
676    let canonical_worktree = worktree_root.canonicalize().map_err(|e| {
677        let file_error = crate::core::file_error::FileOperationError::new(
678            crate::core::file_error::FileOperationContext::new(
679                crate::core::file_error::FileOperation::Canonicalize,
680                worktree_root,
681                "canonicalizing worktree root for transitive dependency".to_string(),
682                "transitive_resolver::strip_git_worktree_prefix_from_parent",
683            ),
684            e,
685        );
686        anyhow::Error::from(file_error)
687    })?;
688
689    // Check if this is a pattern path (contains glob characters)
690    let trans_str = trans_canonical.to_string_lossy();
691    let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
692
693    if is_pattern {
694        // For patterns, canonicalize the directory part while keeping the pattern filename intact
695        let parent_dir = trans_canonical.parent().ok_or_else(|| {
696            anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
697        })?;
698        let filename = trans_canonical.file_name().ok_or_else(|| {
699            anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
700        })?;
701
702        // Canonicalize the directory part
703        let canonical_dir = parent_dir.canonicalize().map_err(|e| {
704            let file_error = crate::core::file_error::FileOperationError::new(
705                crate::core::file_error::FileOperationContext::new(
706                    crate::core::file_error::FileOperation::Canonicalize,
707                    parent_dir,
708                    "canonicalizing pattern directory for Git worktree".to_string(),
709                    "transitive_resolver::strip_git_worktree_prefix_from_parent",
710                ),
711                e,
712            );
713            anyhow::Error::from(file_error)
714        })?;
715
716        // Reconstruct the full path with canonical directory and pattern filename
717        let canonical_pattern = canonical_dir.join(filename);
718
719        // Now strip the worktree prefix
720        canonical_pattern
721            .strip_prefix(&canonical_worktree)
722            .with_context(|| {
723                format!(
724                    "Transitive pattern dep outside parent's worktree: {} not under {}",
725                    canonical_pattern.display(),
726                    canonical_worktree.display()
727                )
728            })
729            .map(|p| p.to_path_buf())
730    } else {
731        trans_canonical
732            .strip_prefix(&canonical_worktree)
733            .with_context(|| {
734                format!(
735                    "Transitive dep outside parent's worktree: {} not under {}",
736                    trans_canonical.display(),
737                    canonical_worktree.display()
738                )
739            })
740            .map(|p| p.to_path_buf())
741    }
742}
743
744/// Determine the tool for a transitive dependency.
745fn determine_transitive_tool(
746    ctx: &TransitiveContext<'_>,
747    parent_dep: &ResourceDependency,
748    dep_spec: &crate::manifest::DependencySpec,
749    parent_resource_type: ResourceType,
750    dep_resource_type: ResourceType,
751) -> Option<String> {
752    if let Some(explicit_tool) = &dep_spec.tool {
753        Some(explicit_tool.clone())
754    } else {
755        let parent_tool = parent_dep
756            .get_tool()
757            .map(str::to_string)
758            .unwrap_or_else(|| ctx.base.manifest.get_default_tool(parent_resource_type));
759        if ctx.base.manifest.is_resource_supported(&parent_tool, dep_resource_type) {
760            Some(parent_tool)
761        } else {
762            Some(ctx.base.manifest.get_default_tool(dep_resource_type))
763        }
764    }
765}
766
767/// Build the final ordered result from the dependency graph.
768fn build_ordered_result(
769    all_deps: Arc<DashMap<DependencyKey, ResourceDependency>>,
770    ordered_nodes: Vec<DependencyNode>,
771) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
772    let mut result = Vec::new();
773    let mut added_keys = HashSet::new();
774
775    tracing::debug!(
776        "Transitive resolution - topological order has {} nodes, all_deps has {} entries",
777        ordered_nodes.len(),
778        all_deps.len()
779    );
780
781    for node in ordered_nodes {
782        tracing::debug!(
783            "Processing ordered node: {}/{} (source: {:?})",
784            node.resource_type,
785            node.name,
786            node.source
787        );
788
789        // Find matching dependency
790        for entry in all_deps.iter() {
791            let (key, dep) = (entry.key(), entry.value());
792            if key.0 == node.resource_type && key.1 == node.name && key.2 == node.source {
793                tracing::debug!(
794                    "  -> Found match in all_deps, adding to result with type {:?}",
795                    node.resource_type
796                );
797                result.push((node.name.clone(), dep.clone(), node.resource_type));
798                added_keys.insert(key.clone());
799                break;
800            }
801        }
802    }
803
804    // Add remaining dependencies that weren't in the graph (no transitive deps)
805    for entry in all_deps.iter() {
806        let (key, dep) = (entry.key(), entry.value());
807        if !added_keys.contains(key) && !dep.is_pattern() {
808            tracing::debug!(
809                "Adding non-graph dependency: {}/{} (source: {:?}) with type {:?}",
810                key.0,
811                key.1,
812                key.2,
813                key.0
814            );
815            result.push((key.1.clone(), dep.clone(), key.0));
816        }
817    }
818
819    tracing::debug!("Transitive resolution returning {} dependencies", result.len());
820
821    Ok(result)
822}
823
824/// Generate unique key for grouping dependencies by source and version.
825pub fn group_key(source: &str, version: &str) -> String {
826    format!("{source}::{version}")
827}
828
829/// Process a single transitive dependency from the queue.
830///
831/// This function extracts the core loop body logic into a standalone async function
832/// that can be executed in parallel batches for improved performance.
833async fn process_single_transitive_dependency<'a>(
834    ctx: TransitiveProcessingContext<'a>,
835) -> Result<()> {
836    let source = ctx.input.dep.get_source().map(std::string::ToString::to_string);
837    // Use resolved tool (with manifest default) to match dependency_processing.rs lookup
838    // If dep has explicit tool, use it; otherwise use manifest's default tool for resource type
839    let tool =
840        Some(ctx.input.dep.get_tool().map(std::string::ToString::to_string).unwrap_or_else(|| {
841            ctx.resolution.ctx_base.manifest.get_default_tool(ctx.input.resource_type)
842        }));
843
844    let key = (
845        ctx.input.resource_type,
846        ctx.input.name.clone(),
847        source.clone(),
848        tool.clone(),
849        ctx.input.variant_hash.clone(),
850    );
851
852    // Build display name for progress tracking
853    let display_name = if source.is_some() {
854        if let Some(version) = ctx.input.dep.get_version() {
855            format!("{}@{}", ctx.input.name, version)
856        } else {
857            format!("{}@HEAD", ctx.input.name)
858        }
859    } else {
860        ctx.input.name.clone()
861    };
862    let progress_key = format!("{}:{}", ctx.input.resource_type, &display_name);
863
864    // Mark as active in progress window
865    if let Some(ref pm) = ctx.progress {
866        pm.mark_item_active(&display_name, &progress_key);
867    }
868
869    tracing::debug!(
870        "[TRANSITIVE] Processing: '{}' (type: {:?}, source: {:?})",
871        ctx.input.name,
872        ctx.input.resource_type,
873        source
874    );
875
876    // Check if this queue entry is stale (superseded by conflict resolution)
877    // CRITICAL: Extract version comparison result before releasing DashMap lock.
878    // We must not hold DashMap read locks while acquiring the queue Mutex,
879    // as this creates a potential AB-BA deadlock with other parallel tasks.
880    let is_stale = ctx
881        .shared
882        .all_deps
883        .get(&key)
884        .map(|current_dep| current_dep.get_version() != ctx.input.dep.get_version())
885        .unwrap_or(false);
886
887    if is_stale {
888        tracing::debug!("[TRANSITIVE] Skipped stale: '{}'", ctx.input.name);
889        // DashMap lock is released - progress update uses atomic counter (no lock needed)
890        if let Some(ref pm) = ctx.progress {
891            let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
892            let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
893            pm.mark_item_complete(
894                &progress_key,
895                Some(&display_name),
896                completed,
897                total,
898                "Scanning dependencies",
899            );
900        }
901        return Ok(());
902    }
903
904    if ctx.shared.processed.contains_key(&key) {
905        tracing::debug!("[TRANSITIVE] Already processed: '{}'", ctx.input.name);
906        if let Some(ref pm) = ctx.progress {
907            let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
908            let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
909            pm.mark_item_complete(
910                &progress_key,
911                Some(&display_name),
912                completed,
913                total,
914                "Scanning dependencies",
915            );
916        }
917        return Ok(());
918    }
919
920    ctx.shared.processed.insert(key.clone(), ());
921
922    // Handle pattern dependencies by expanding them to concrete files
923    if ctx.input.dep.is_pattern() {
924        tracing::debug!("[TRANSITIVE] Expanding pattern: '{}'", ctx.input.name);
925        match ctx
926            .resolution
927            .services
928            .pattern_service
929            .expand_pattern(
930                ctx.resolution.core,
931                &ctx.input.dep,
932                ctx.input.resource_type,
933                ctx.shared.prepared_versions.as_ref(),
934            )
935            .await
936        {
937            Ok(concrete_deps) => {
938                // CRITICAL: Collect items to add to queue BEFORE acquiring queue lock.
939                // We must not hold DashMap entry locks while acquiring the queue Mutex,
940                // as this creates a potential AB-BA deadlock with other parallel tasks.
941                let mut items_to_queue = Vec::new();
942
943                for (concrete_name, concrete_dep) in concrete_deps {
944                    ctx.shared.pattern_alias_map.insert(
945                        (ctx.input.resource_type, concrete_name.clone()),
946                        ctx.input.name.clone(),
947                    );
948
949                    let concrete_source =
950                        concrete_dep.get_source().map(std::string::ToString::to_string);
951                    let concrete_tool =
952                        concrete_dep.get_tool().map(std::string::ToString::to_string);
953                    let concrete_variant_hash = compute_dependency_variant_hash(&concrete_dep);
954                    let concrete_key = (
955                        ctx.input.resource_type,
956                        concrete_name.clone(),
957                        concrete_source,
958                        concrete_tool,
959                        concrete_variant_hash.clone(),
960                    );
961
962                    // Check and insert atomically, but DON'T hold entry lock while queuing
963                    match ctx.shared.all_deps.entry(concrete_key) {
964                        dashmap::mapref::entry::Entry::Vacant(e) => {
965                            e.insert(concrete_dep.clone());
966                            // Collect for later queue insertion (after DashMap entry is released)
967                            items_to_queue.push((
968                                concrete_name,
969                                concrete_dep,
970                                Some(ctx.input.resource_type),
971                                concrete_variant_hash,
972                            ));
973                        }
974                        dashmap::mapref::entry::Entry::Occupied(mut e) => {
975                            // Entry exists - check if we should replace with semver version
976                            let existing = e.get();
977                            if should_replace_existing(existing, &concrete_dep) {
978                                tracing::debug!(
979                                    "[PATTERN] Replacing existing dep '{}' with semver version",
980                                    concrete_name
981                                );
982                                e.insert(concrete_dep.clone());
983                                items_to_queue.push((
984                                    concrete_name,
985                                    concrete_dep,
986                                    Some(ctx.input.resource_type),
987                                    concrete_variant_hash,
988                                ));
989                            }
990                        }
991                    }
992                    // DashMap entry lock is released here at end of match scope
993                }
994
995                // Now safely acquire queue lock without holding any DashMap locks
996                if !items_to_queue.is_empty() {
997                    let items_count = items_to_queue.len();
998                    let mut queue =
999                        acquire_mutex_with_timeout(&ctx.shared.queue, "transitive_queue").await?;
1000                    queue.extend(items_to_queue);
1001                    // Update atomic counter after extending queue
1002                    ctx.shared.queue_len.fetch_add(items_count, Ordering::SeqCst);
1003                }
1004            }
1005            Err(e) => {
1006                anyhow::bail!("Failed to expand pattern '{}': {}", ctx.input.dep.get_path(), e);
1007            }
1008        }
1009        // Pattern expansion complete - progress update uses atomic counter (no lock needed)
1010        if let Some(ref pm) = ctx.progress {
1011            let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
1012            let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
1013            pm.mark_item_complete(
1014                &progress_key,
1015                Some(&display_name),
1016                completed,
1017                total,
1018                "Scanning dependencies",
1019            );
1020        }
1021        return Ok(());
1022    }
1023
1024    // Fetch resource content for metadata extraction
1025    // For skills, we need to read the SKILL.md file inside the directory
1026    let content = if ctx.input.resource_type == ResourceType::Skill {
1027        // Create a modified dependency that points to SKILL.md inside the skill directory
1028        let skill_md_dep = create_skill_md_dependency(&ctx.input.dep);
1029        ResourceFetchingService::fetch_content(
1030            ctx.resolution.core,
1031            &skill_md_dep,
1032            ctx.resolution.services.version_service,
1033        )
1034        .await
1035        .with_context(|| {
1036            format!(
1037                "Failed to fetch SKILL.md for skill '{}' ({})",
1038                ctx.input.name,
1039                ctx.input.dep.get_path()
1040            )
1041        })?
1042    } else {
1043        ResourceFetchingService::fetch_content(
1044            ctx.resolution.core,
1045            &ctx.input.dep,
1046            ctx.resolution.services.version_service,
1047        )
1048        .await
1049        .with_context(|| {
1050            format!(
1051                "Failed to fetch resource '{}' ({}) for transitive deps",
1052                ctx.input.name,
1053                ctx.input.dep.get_path()
1054            )
1055        })?
1056    };
1057
1058    // Note: With single-pass rendering, we no longer need to wrap non-templated
1059    // content in guards. Dependencies are rendered once with their own context
1060    // and embedded as-is.
1061
1062    tracing::debug!(
1063        "[TRANSITIVE] Fetched content for '{}' ({} bytes)",
1064        ctx.input.name,
1065        content.len()
1066    );
1067
1068    // Build complete template_vars including global project config for metadata extraction
1069    // This ensures transitive dependencies can use template variables like {{ agpm.project.language }}
1070    let variant_inputs_value = super::lockfile_builder::build_merged_variant_inputs(
1071        ctx.resolution.ctx_base.manifest,
1072        &ctx.input.dep,
1073    );
1074    let variant_inputs = Some(&variant_inputs_value);
1075
1076    // Extract metadata from the resource with complete variant_inputs
1077    // For skills, use SKILL.md path so extractor recognizes it as markdown
1078    let path = if ctx.input.resource_type == ResourceType::Skill {
1079        PathBuf::from(format!("{}/SKILL.md", ctx.input.dep.get_path().trim_end_matches('/')))
1080    } else {
1081        PathBuf::from(ctx.input.dep.get_path())
1082    };
1083    let metadata = MetadataExtractor::extract(
1084        &path,
1085        &content,
1086        variant_inputs,
1087        ctx.resolution.ctx_base.operation_context.map(|arc| arc.as_ref()),
1088    )?;
1089
1090    tracing::debug!(
1091        "[DEBUG] Extracted metadata for '{}': has_deps={}",
1092        ctx.input.name,
1093        metadata.get_dependencies().is_some()
1094    );
1095
1096    // Process transitive dependencies if present
1097    if let Some(deps_map) = metadata.get_dependencies() {
1098        tracing::debug!(
1099            "[DEBUG] Found {} dependency type(s) for '{}': {:?}",
1100            deps_map.len(),
1101            ctx.input.name,
1102            deps_map.keys().collect::<Vec<_>>()
1103        );
1104
1105        // CRITICAL: Collect items to queue BEFORE acquiring queue lock.
1106        // We must not hold DashMap entry locks while acquiring the queue Mutex,
1107        // as this creates a potential AB-BA deadlock with other parallel tasks.
1108        let mut items_to_queue = Vec::new();
1109
1110        // CRITICAL: Collect graph edges to batch-insert AFTER the loop.
1111        // Acquiring the graph mutex inside the loop creates high contention
1112        // and potential deadlocks with DashMap operations.
1113        let mut graph_edges: Vec<(DependencyNode, DependencyNode)> = Vec::new();
1114
1115        for (dep_resource_type_str, dep_specs) in deps_map {
1116            let dep_resource_type: ResourceType =
1117                dep_resource_type_str.parse().unwrap_or(ResourceType::Snippet);
1118
1119            for dep_spec in dep_specs {
1120                // Create a temporary TransitiveContext for this call
1121                // Note: conflict_detector is not used in parallel code (was removed in Phase 4)
1122                let mut dummy_conflict_detector = ConflictDetector::new();
1123                let temp_ctx = super::types::TransitiveContext {
1124                    base: *ctx.resolution.ctx_base,
1125                    dependency_map: ctx.shared.dependency_map,
1126                    transitive_custom_names: ctx.shared.custom_names,
1127                    conflict_detector: &mut dummy_conflict_detector,
1128                    manifest_overrides: ctx.resolution.manifest_overrides,
1129                };
1130
1131                // Process each transitive dependency spec
1132                let (trans_dep, trans_name) =
1133                    process_transitive_dependency_spec(TransitiveDepProcessingParams {
1134                        ctx: &temp_ctx,
1135                        core: ctx.resolution.core,
1136                        parent_dep: &ctx.input.dep,
1137                        dep_resource_type,
1138                        parent_resource_type: ctx.input.resource_type,
1139                        parent_name: &ctx.input.name,
1140                        dep_spec,
1141                        version_service: ctx.resolution.services.version_service,
1142                        prepared_versions: ctx.shared.prepared_versions,
1143                    })
1144                    .await?;
1145
1146                let trans_source = trans_dep.get_source().map(std::string::ToString::to_string);
1147                let trans_tool = trans_dep.get_tool().map(std::string::ToString::to_string);
1148                let trans_variant_hash = compute_dependency_variant_hash(&trans_dep);
1149
1150                // Store custom name if provided
1151                if let Some(custom_name) = &dep_spec.name {
1152                    let trans_key = (
1153                        dep_resource_type,
1154                        trans_name.clone(),
1155                        trans_source.clone(),
1156                        trans_tool.clone(),
1157                        trans_variant_hash.clone(),
1158                    );
1159                    ctx.shared.custom_names.insert(trans_key, custom_name.clone());
1160                    tracing::debug!(
1161                        "Storing custom name '{}' for transitive dep '{}'",
1162                        custom_name,
1163                        trans_name
1164                    );
1165                }
1166
1167                // Collect edge for dependency graph (batch-insert after loop)
1168                let from_node = DependencyNode::with_source(
1169                    ctx.input.resource_type,
1170                    &ctx.input.name,
1171                    source.clone(),
1172                );
1173                let to_node = DependencyNode::with_source(
1174                    dep_resource_type,
1175                    &trans_name,
1176                    trans_source.clone(),
1177                );
1178                graph_edges.push((from_node, to_node));
1179
1180                // Track in dependency map
1181                let from_key = (
1182                    ctx.input.resource_type,
1183                    ctx.input.name.clone(),
1184                    source.clone(),
1185                    tool.clone(),
1186                    ctx.input.variant_hash.clone(),
1187                );
1188                let dep_ref =
1189                    LockfileDependencyRef::local(dep_resource_type, trans_name.clone(), None)
1190                        .to_string();
1191                tracing::debug!(
1192                    "[DEBUG] Adding to dependency_map: parent='{}' (type={:?}, source={:?}, tool={:?}, hash={}), child='{}' (type={:?})",
1193                    ctx.input.name,
1194                    ctx.input.resource_type,
1195                    source,
1196                    tool,
1197                    &ctx.input.variant_hash[..8],
1198                    dep_ref,
1199                    dep_resource_type
1200                );
1201                ctx.shared.dependency_map.entry(from_key).or_default().push(dep_ref);
1202
1203                // DON'T add to conflict detector yet - we'll do it after SHA resolution
1204                // (Removed: add_to_conflict_detector call)
1205
1206                // Check for version conflicts
1207                let trans_key = (
1208                    dep_resource_type,
1209                    trans_name.clone(),
1210                    trans_source.clone(),
1211                    trans_tool.clone(),
1212                    trans_variant_hash.clone(),
1213                );
1214
1215                tracing::debug!(
1216                    "[TRANSITIVE] Found transitive dep '{}' (type: {:?}, tool: {:?}, parent: {})",
1217                    trans_name,
1218                    dep_resource_type,
1219                    trans_tool,
1220                    ctx.input.name
1221                );
1222
1223                // Check if we already have this dependency
1224                // Use entry API to atomically check and potentially update
1225                match ctx.shared.all_deps.entry(trans_key) {
1226                    dashmap::mapref::entry::Entry::Vacant(e) => {
1227                        // No existing entry, add the dependency
1228                        tracing::debug!(
1229                            "Adding transitive dep '{}' (parent: {})",
1230                            trans_name,
1231                            ctx.input.name
1232                        );
1233                        e.insert(trans_dep.clone());
1234                        // Collect for later queue insertion (after DashMap entry is released)
1235                        items_to_queue.push((
1236                            trans_name,
1237                            trans_dep,
1238                            Some(dep_resource_type),
1239                            trans_variant_hash,
1240                        ));
1241                    }
1242                    dashmap::mapref::entry::Entry::Occupied(mut e) => {
1243                        // Dependency already exists - check if we should replace it
1244                        // Prefer semver versions over git refs (e.g., v1.0.0 wins over main)
1245                        let existing = e.get();
1246                        if should_replace_existing(existing, &trans_dep) {
1247                            tracing::debug!(
1248                                "[TRANSITIVE] Replacing existing dep '{}' (version: {:?}) with semver version {:?}",
1249                                trans_name,
1250                                existing.get_version(),
1251                                trans_dep.get_version()
1252                            );
1253                            e.insert(trans_dep.clone());
1254                            // Re-queue to process with updated version
1255                            items_to_queue.push((
1256                                trans_name,
1257                                trans_dep,
1258                                Some(dep_resource_type),
1259                                trans_variant_hash,
1260                            ));
1261                        } else {
1262                            tracing::debug!(
1263                                "[TRANSITIVE] Keeping existing dep '{}' (version: {:?} vs new {:?})",
1264                                trans_name,
1265                                existing.get_version(),
1266                                trans_dep.get_version()
1267                            );
1268                        }
1269                    }
1270                }
1271                // DashMap entry lock is released here at end of match scope
1272            }
1273        }
1274
1275        // Batch-insert all graph edges after loops complete (single mutex acquisition)
1276        if !graph_edges.is_empty() {
1277            let mut graph =
1278                acquire_mutex_with_timeout(&ctx.shared.graph, "dependency_graph").await?;
1279            for (from_node, to_node) in graph_edges {
1280                graph.add_dependency(from_node, to_node);
1281            }
1282        }
1283
1284        // Now safely acquire queue lock without holding any DashMap locks
1285        if !items_to_queue.is_empty() {
1286            let items_count = items_to_queue.len();
1287            let mut queue =
1288                acquire_mutex_with_timeout(&ctx.shared.queue, "transitive_queue").await?;
1289            queue.extend(items_to_queue);
1290            // Update atomic counter after extending queue
1291            ctx.shared.queue_len.fetch_add(items_count, Ordering::SeqCst);
1292        }
1293    }
1294
1295    // Mark item as complete in progress window - uses atomic counter (no lock needed)
1296    if let Some(ref pm) = ctx.progress {
1297        let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
1298        let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
1299        pm.mark_item_complete(
1300            &progress_key,
1301            Some(&display_name),
1302            completed,
1303            total,
1304            "Scanning dependencies",
1305        );
1306    }
1307
1308    Ok(())
1309}
1310
1311/// Service-based wrapper for transitive dependency resolution.
1312///
1313/// This provides a simpler API for internal use that takes service references
1314/// directly instead of requiring closure-based dependency injection.
1315pub async fn resolve_with_services(
1316    params: TransitiveResolutionParams<'_>,
1317) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
1318    let TransitiveResolutionParams {
1319        ctx,
1320        core,
1321        base_deps,
1322        enable_transitive,
1323        prepared_versions,
1324        pattern_alias_map,
1325        services,
1326        progress,
1327    } = params;
1328    // Clear state from any previous resolution
1329    ctx.dependency_map.clear();
1330
1331    if !enable_transitive {
1332        return Ok(base_deps.to_vec());
1333    }
1334
1335    let graph = Arc::new(tokio::sync::Mutex::new(DependencyGraph::new()));
1336    let all_deps: Arc<DashMap<DependencyKey, ResourceDependency>> = Arc::new(DashMap::new());
1337    let processed: Arc<DashMap<DependencyKey, ()>> = Arc::new(DashMap::new()); // Simulates HashSet
1338
1339    // Type alias to reduce complexity
1340    type QueueItem = (String, ResourceDependency, Option<ResourceType>, String);
1341    #[allow(clippy::type_complexity)]
1342    let queue: Arc<tokio::sync::Mutex<Vec<QueueItem>>> =
1343        Arc::new(tokio::sync::Mutex::new(Vec::new()));
1344    // Atomic counter for queue length - enables lock-free progress tracking
1345    let queue_len = Arc::new(AtomicUsize::new(0));
1346
1347    // Add initial dependencies to queue with their threaded types
1348    {
1349        let mut queue_guard = acquire_mutex_with_timeout(&queue, "transitive_queue").await?;
1350        for (name, dep, resource_type) in base_deps {
1351            let source = dep.get_source().map(std::string::ToString::to_string);
1352            // Use resolved tool (with manifest default) to match dependency_processing.rs lookup
1353            let tool = Some(
1354                dep.get_tool()
1355                    .map(std::string::ToString::to_string)
1356                    .unwrap_or_else(|| ctx.base.manifest.get_default_tool(*resource_type)),
1357            );
1358
1359            // Compute variant_hash from MERGED variant_inputs (dep + global config)
1360            // This ensures consistency with how LockedResource computes its hash
1361            let merged_variant_inputs =
1362                super::lockfile_builder::build_merged_variant_inputs(ctx.base.manifest, dep);
1363            let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
1364                .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
1365
1366            tracing::debug!(
1367                "[DEBUG] Adding base dep to queue: '{}' (type: {:?}, source: {:?}, tool: {:?}, is_local: {})",
1368                name,
1369                resource_type,
1370                source,
1371                tool,
1372                dep.is_local()
1373            );
1374            // Store pre-computed hash in queue to avoid duplicate computation
1375            queue_guard.push((
1376                name.clone(),
1377                dep.clone(),
1378                Some(*resource_type),
1379                variant_hash.clone(),
1380            ));
1381            all_deps
1382                .insert((*resource_type, name.clone(), source, tool, variant_hash), dep.clone());
1383        }
1384        // Update atomic queue length counter
1385        queue_len.store(queue_guard.len(), Ordering::SeqCst);
1386    }
1387
1388    // Track progress: total items to process = base_deps + discovered transitives
1389    let completed_counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1390
1391    // Calculate concurrency based on CPU cores
1392    let cores = std::thread::available_parallelism().map(std::num::NonZero::get).unwrap_or(4);
1393    let max_concurrent = std::cmp::max(10, cores * 2);
1394
1395    // Extract ctx references for parallel access (conflict_detector needs &mut, so we keep it outside)
1396    let ctx_dependency_map = ctx.dependency_map;
1397    let ctx_custom_names = ctx.transitive_custom_names;
1398    let ctx_base = &ctx.base;
1399    let ctx_manifest_overrides = ctx.manifest_overrides;
1400
1401    // Process queue in parallel batches to discover transitive dependencies
1402    loop {
1403        // Extract batch from queue (drain from end, same as serial pop order)
1404        let batch: Vec<QueueEntry> = {
1405            let mut q = acquire_mutex_with_timeout(&queue, "transitive_queue").await?;
1406            let current_queue_len = q.len();
1407            let batch_size = std::cmp::min(max_concurrent, current_queue_len);
1408            if batch_size == 0 {
1409                break; // Queue empty
1410            }
1411            // Drain from end and reverse to maintain LIFO ordering like serial version
1412            let mut batch_vec =
1413                q.drain(current_queue_len.saturating_sub(batch_size)..).collect::<Vec<_>>();
1414            batch_vec.reverse(); // Reverse to process in same order as serial (last added first)
1415            // Update atomic counter after draining from queue
1416            queue_len.fetch_sub(batch_vec.len(), Ordering::SeqCst);
1417            batch_vec
1418        };
1419
1420        // Process batch in parallel
1421        let batch_futures: Vec<_> = batch
1422            .into_iter()
1423            .map(|(name, dep, resource_type, variant_hash)| {
1424                // Clone Arc refs for concurrent access
1425                let graph_clone = Arc::clone(&graph);
1426                let all_deps_clone = Arc::clone(&all_deps);
1427                let processed_clone = Arc::clone(&processed);
1428                let queue_clone = Arc::clone(&queue);
1429                let queue_len_clone = Arc::clone(&queue_len);
1430                let pattern_alias_map_clone = Arc::clone(pattern_alias_map);
1431                let progress_clone = progress.clone();
1432                let counter_clone = Arc::clone(&completed_counter);
1433                let prepared_versions_clone = Arc::clone(prepared_versions);
1434                let dependency_map_clone = ctx_dependency_map;
1435                let custom_names_clone = ctx_custom_names;
1436                let manifest_overrides_clone = ctx_manifest_overrides;
1437
1438                async move {
1439                    let resource_type = resource_type
1440                        .expect("resource_type should always be threaded through queue");
1441
1442                    // Construct the processing context
1443                    let ctx = TransitiveProcessingContext {
1444                        input: TransitiveInput {
1445                            name,
1446                            dep,
1447                            resource_type,
1448                            variant_hash,
1449                        },
1450                        shared: TransitiveSharedState {
1451                            graph: graph_clone,
1452                            all_deps: all_deps_clone,
1453                            processed: processed_clone,
1454                            queue: queue_clone,
1455                            queue_len: queue_len_clone,
1456                            pattern_alias_map: pattern_alias_map_clone,
1457                            completed_counter: counter_clone,
1458                            dependency_map: dependency_map_clone,
1459                            custom_names: custom_names_clone,
1460                            prepared_versions: &prepared_versions_clone,
1461                        },
1462                        resolution: TransitiveResolutionContext {
1463                            ctx_base,
1464                            manifest_overrides: manifest_overrides_clone,
1465                            core,
1466                            services,
1467                        },
1468                        progress: progress_clone,
1469                    };
1470
1471                    process_single_transitive_dependency(ctx).await
1472                }
1473            })
1474            .collect();
1475
1476        // Execute batch concurrently with timeout to prevent indefinite blocking
1477        let timeout_duration = batch_operation_timeout();
1478        let results = tokio::time::timeout(timeout_duration, join_all(batch_futures))
1479            .await
1480            .with_context(|| {
1481                format!(
1482                    "Batch transitive resolution timed out after {:?} - possible deadlock",
1483                    timeout_duration
1484                )
1485            })?;
1486
1487        // Check for errors
1488        for result in results {
1489            result?;
1490        }
1491    }
1492
1493    // Check for circular dependencies
1494    acquire_mutex_with_timeout(&graph, "dependency_graph").await?.detect_cycles()?;
1495
1496    // Get topological order
1497    let ordered_nodes =
1498        acquire_mutex_with_timeout(&graph, "dependency_graph").await?.topological_order()?;
1499
1500    // Build result with topologically ordered dependencies
1501    build_ordered_result(all_deps, ordered_nodes)
1502}
1503
1504/// Create a modified dependency that points to SKILL.md inside a skill directory.
1505///
1506/// Skills are directory-based resources, but we need to read their SKILL.md file
1507/// for metadata extraction. This function creates a new dependency with the path
1508/// modified to point to the SKILL.md file.
1509fn create_skill_md_dependency(dep: &ResourceDependency) -> ResourceDependency {
1510    match dep {
1511        ResourceDependency::Simple(path) => {
1512            // For simple deps, append /SKILL.md to the path
1513            let skill_md_path = format!("{}/SKILL.md", path.trim_end_matches('/'));
1514            ResourceDependency::Simple(skill_md_path)
1515        }
1516        ResourceDependency::Detailed(detailed) => {
1517            // For detailed deps, create a new detailed dep with modified path
1518            let skill_md_path = format!("{}/SKILL.md", detailed.path.trim_end_matches('/'));
1519            ResourceDependency::Detailed(Box::new(DetailedDependency {
1520                path: skill_md_path,
1521                source: detailed.source.clone(),
1522                version: detailed.version.clone(),
1523                branch: detailed.branch.clone(),
1524                rev: detailed.rev.clone(),
1525                command: detailed.command.clone(),
1526                args: detailed.args.clone(),
1527                target: detailed.target.clone(),
1528                filename: detailed.filename.clone(),
1529                dependencies: detailed.dependencies.clone(),
1530                tool: detailed.tool.clone(),
1531                flatten: detailed.flatten,
1532                install: detailed.install,
1533                template_vars: detailed.template_vars.clone(),
1534            }))
1535        }
1536    }
1537}