1use anyhow::{Context as _, Result};
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::path::Path;
9use std::str::FromStr;
10
11use crate::core::ResourceType;
12use crate::lockfile::lockfile_dependency_ref::LockfileDependencyRef;
13use crate::lockfile::{LockedResource, ResourceId};
14
15use super::extractors::{DependencyExtractor, create_dependency_ref_string};
16use crate::templating::cache::RenderCacheKey;
17use crate::templating::context::DependencyData;
18use crate::templating::renderer::TemplateRenderer;
19use crate::templating::utils::to_native_path_display;
20
21pub(crate) async fn build_dependencies_data<T: DependencyExtractor>(
37 extractor: &T,
38 current_resource: &LockedResource,
39 rendering_stack: &mut HashSet<String>,
40) -> Result<BTreeMap<String, BTreeMap<String, DependencyData>>> {
41 let mut deps = BTreeMap::new();
42
43 let dependency_specs =
46 extractor.extract_dependency_specs(current_resource).await.with_context(|| {
47 format!(
48 "Failed to extract dependency specifications from resource '{}' (type: {:?})",
49 current_resource.name, current_resource.resource_type
50 )
51 })?;
52
53 let get_key_names =
55 |resource: &LockedResource, dep_type: &ResourceType| -> (String, String, String, String) {
56 let type_str_plural = dep_type.to_plural().to_string();
57 let type_str_singular = dep_type.to_string();
58
59 let key_name = if resource.name.contains('/') || resource.name.contains('\\') {
63 std::path::Path::new(&resource.name)
65 .file_stem()
66 .and_then(|s| s.to_str())
67 .unwrap_or(&resource.name)
68 .to_string()
69 } else {
70 resource.name.clone()
72 };
73
74 let sanitized_key = key_name.replace('-', "_");
77
78 (type_str_plural, type_str_singular, key_name, sanitized_key)
79 };
80
81 let mut resources_to_process: Vec<(&LockedResource, ResourceType, bool)> = Vec::new();
84 let mut visited_dep_ids = HashSet::new();
85
86 for dep_ref in current_resource.parsed_dependencies() {
87 let dep_id = dep_ref.to_string();
89
90 if !visited_dep_ids.insert(dep_id.clone()) {
92 continue;
93 }
94
95 let resource_type = dep_ref.resource_type;
96 let name = &dep_ref.path;
97
98 let dep_spec = {
103 let normalized_path = {
105 let path = std::path::Path::new(&dep_ref.path);
106 let normalized = crate::utils::normalize_path(path);
107 normalized.to_string_lossy().to_string()
108 };
109
110 let normalized_dep_ref = LockfileDependencyRef::new(
113 dep_ref.source.clone(),
114 dep_ref.resource_type,
115 normalized_path,
116 None, );
118 let normalized_dep_id = normalized_dep_ref.to_string();
119
120 dependency_specs.get(&normalized_dep_id)
121 };
122
123 tracing::debug!(
124 "Looking up dep_spec for dep_id='{}', found={}, available_keys={:?}",
125 dep_id,
126 dep_spec.is_some(),
127 dependency_specs.keys().collect::<Vec<_>>()
128 );
129
130 let dep_tool_str =
133 dep_spec.and_then(|spec| spec.tool.as_deref()).or(current_resource.tool.as_deref());
134
135 let dep_source_str = dep_ref.source.as_deref().or(current_resource.source.as_deref());
138
139 let dep_resource_id_with_parent_hash = ResourceId::new(
142 name.clone(),
143 dep_source_str,
144 dep_tool_str,
145 resource_type,
146 current_resource.variant_inputs.hash().to_string(),
147 );
148
149 let hash_str = current_resource.variant_inputs.hash();
150 let hash_prefix = if hash_str.len() > 8 {
151 &hash_str[..8]
152 } else {
153 hash_str
154 };
155
156 tracing::debug!(
157 "[DEBUG] Template context looking up: name='{}', type={:?}, source={:?}, tool={:?}, hash={}",
158 name,
159 resource_type,
160 dep_source_str,
161 dep_tool_str,
162 hash_prefix
163 );
164
165 let mut dep_resource =
168 extractor.lockfile().find_resource_by_id(&dep_resource_id_with_parent_hash);
169
170 if dep_resource.is_none() {
172 let dep_resource_id_empty_hash = ResourceId::new(
173 name.clone(),
174 dep_source_str,
175 dep_tool_str,
176 resource_type,
177 crate::resolver::lockfile_builder::VariantInputs::default().hash().to_string(),
178 );
179 dep_resource = extractor.lockfile().find_resource_by_id(&dep_resource_id_empty_hash);
180
181 if dep_resource.is_some() {
182 tracing::debug!(
183 " [DIRECT MANIFEST DEP] Found dependency '{}' with empty variant_hash (direct manifest dependency)",
184 name
185 );
186 }
187 }
188
189 if let Some(dep_resource) = dep_resource {
190 resources_to_process.push((dep_resource, resource_type, true));
192
193 tracing::debug!(
194 " [DIRECT DEP] Found dependency '{}' (tool: {:?}) for '{}'",
195 name,
196 dep_tool_str,
197 current_resource.name
198 );
199 } else {
200 tracing::warn!(
201 "Dependency '{}' (type: {:?}, tool: {:?}) not found in lockfile for resource '{}'",
202 name,
203 resource_type,
204 dep_tool_str,
205 current_resource.name
206 );
207 }
208 }
209
210 tracing::debug!(
211 "Building dependencies data with {} direct dependencies for '{}'",
212 resources_to_process.len(),
213 current_resource.name
214 );
215
216 resources_to_process.sort_by(|a, b| {
221 use std::cmp::Ordering;
222 match a.1.cmp(&b.1) {
224 Ordering::Equal => {
225 match a.0.name.cmp(&b.0.name) {
227 Ordering::Equal => {
228 b.2.cmp(&a.2) }
231 other => other,
232 }
233 }
234 other => other,
235 }
236 });
237
238 for (resource, dep_type, is_dep) in &resources_to_process {
240 tracing::debug!(
241 " [LOCKFILE] Resource: {} (type: {:?}, install: {:?}, is_dependency: {})",
242 resource.name,
243 dep_type,
244 resource.install,
245 is_dep
246 );
247 }
248
249 let current_resource_id = create_dependency_ref_string(
251 current_resource.source.as_deref(),
252 current_resource.resource_type,
253 ¤t_resource.name,
254 current_resource.version.as_deref(),
255 );
256
257 let dependency_hash = {
260 use std::collections::hash_map::DefaultHasher;
261 use std::hash::{Hash, Hasher};
262
263 let mut hasher = DefaultHasher::new();
264
265 for (dep_id, spec) in &dependency_specs {
267 dep_id.hash(&mut hasher);
268 if let Some(tool) = &spec.tool {
269 tool.hash(&mut hasher);
270 }
271 if let Some(version) = &spec.version {
272 version.hash(&mut hasher);
273 }
274 spec.path.hash(&mut hasher);
275 }
276
277 format!("{:x}", hasher.finish())
278 };
279
280 for (resource, dep_type, is_dependency) in &resources_to_process {
282 let resource_id = create_dependency_ref_string(
283 resource.source.as_deref(),
284 *dep_type,
285 &resource.name,
286 resource.version.as_deref(),
287 );
288
289 if resource_id == current_resource_id {
291 tracing::debug!(
292 " Skipping current resource: {} (preventing self-reference)",
293 resource.name
294 );
295 continue;
296 }
297
298 tracing::debug!(" Processing resource: {} ({})", resource.name, dep_type);
299
300 let (type_str_plural, type_str_singular, _key_name, sanitized_key) =
301 get_key_names(resource, dep_type);
302
303 let (raw_content, has_templating) = match extractor.extract_content(resource).await {
308 Some((content, templating)) => (Some(content), templating),
309 None => (None, false),
310 };
311
312 let should_render = *is_dependency && raw_content.is_some() && has_templating;
316
317 let final_content: String = if should_render {
319 let cache_key = RenderCacheKey::new(
324 resource.path.clone(),
325 *dep_type,
326 resource.tool.clone(),
327 resource.variant_inputs.hash().to_string(),
328 resource.resolved_commit.clone(),
329 dependency_hash.clone(),
330 );
331
332 let cache_result = extractor
334 .render_cache()
335 .lock()
336 .map_err(|e| {
337 anyhow::anyhow!(
338 "Render cache lock poisoned for resource '{}': {}. \
339 This indicates a panic occurred while holding the lock.",
340 resource.name,
341 e
342 )
343 })?
344 .get(&cache_key)
345 .cloned(); if let Some(cached_content) = cache_result {
348 tracing::debug!("Render cache hit for '{}' ({})", resource.name, dep_type);
349 cached_content
350 } else {
351 tracing::debug!(
353 "Render cache miss for '{}' ({}), rendering...",
354 resource.name,
355 dep_type
356 );
357
358 let dep_id = create_dependency_ref_string(
360 resource.source.as_deref(),
361 *dep_type,
362 &resource.name,
363 resource.version.as_deref(),
364 );
365 if rendering_stack.contains(&dep_id) {
366 let chain: Vec<String> = rendering_stack.iter().cloned().collect();
367 anyhow::bail!(
368 "Circular dependency detected while rendering '{}'. \
369 Dependency chain: {} -> {}",
370 resource.name,
371 chain.join(" -> "),
372 dep_id
373 );
374 }
375
376 rendering_stack.insert(dep_id.clone());
378
379 let dep_resource_id = ResourceId::from_resource(resource);
381 let render_result = Box::pin(extractor.build_context_with_visited(
382 &dep_resource_id,
383 resource.variant_inputs.json(),
384 rendering_stack,
385 ))
386 .await;
387
388 rendering_stack.remove(&dep_id);
390
391 match render_result {
392 Ok(dep_context) => {
393 if let Some(content) = raw_content {
395 let mut renderer = TemplateRenderer::new(
396 true,
397 extractor.project_dir().clone(),
398 None,
399 )
400 .with_context(|| {
401 format!(
402 "Failed to create template renderer for dependency '{}' (type: {:?})",
403 resource.name,
404 dep_type
405 )
406 })?;
407
408 let metadata = crate::templating::renderer::RenderingMetadata {
410 resource_name: resource.name.clone(),
411 resource_type: *dep_type,
412 dependency_chain: vec![], source_path: None,
414 depth: rendering_stack.len(),
415 };
416
417 let rendered = renderer
418 .render_template(&content, &dep_context, Some(&metadata))
419 .with_context(|| {
420 format!(
421 "Failed to render dependency '{}' (type: {:?}). \
422 This is a HARD FAILURE - dependency content MUST render successfully.\n\
423 Resource: {} (source: {}, path: {})",
424 resource.name,
425 dep_type,
426 resource.name,
427 resource.source.as_deref().unwrap_or("local"),
428 resource.path
429 )
430 })?;
431
432 let final_content = if resource.path.ends_with(".md") {
434 match crate::markdown::MarkdownDocument::parse(&rendered) {
435 Ok(doc) => doc.content,
436 Err(_) => {
437 let frontmatter_parser =
439 crate::markdown::frontmatter::FrontmatterParser::new();
440 frontmatter_parser.strip_frontmatter(&rendered)
441 }
442 }
443 } else {
444 rendered
445 };
446
447 tracing::debug!(
448 "Successfully rendered dependency content for '{}'",
449 resource.name
450 );
451
452 if let Ok(mut cache) = extractor.render_cache().lock() {
454 cache.insert(cache_key.clone(), final_content.clone());
455 tracing::debug!(
456 "Stored rendered content in cache for '{}'",
457 resource.name
458 );
459 }
460
461 final_content
462 } else {
463 String::new()
465 }
466 }
467 Err(e) => {
468 return Err(e.context(format!(
470 "Failed to build template context for dependency '{}' (type: {:?}). \
471 This is a HARD FAILURE - all dependencies must have valid contexts.\n\
472 Resource: {} (source: {}, path: {})",
473 resource.name,
474 dep_type,
475 resource.name,
476 resource.source.as_deref().unwrap_or("local"),
477 resource.path
478 )));
479 }
480 }
481 }
482 } else {
483 raw_content.unwrap_or_default()
487 };
488
489 let dependency_data = DependencyData {
491 resource_type: type_str_singular.clone(),
492 name: resource.name.clone(),
493 install_path: to_native_path_display(&resource.installed_at),
494 source: resource.source.clone(),
495 version: resource.version.clone(),
496 resolved_commit: resource.resolved_commit.clone(),
497 checksum: resource.checksum.clone(),
498 path: resource.path.clone(),
499 content: final_content.clone(),
500 };
501
502 let type_deps: &mut BTreeMap<String, DependencyData> =
504 deps.entry(type_str_plural.clone()).or_insert_with(BTreeMap::new);
505 type_deps.insert(sanitized_key.clone(), dependency_data);
506
507 tracing::debug!(
508 " Added resource: {}[{}] -> {}",
509 type_str_plural,
510 sanitized_key,
511 resource.path
512 );
513 }
514
515 tracing::debug!(
518 "Extracting custom dependency names for direct deps of: '{}'",
519 current_resource.name
520 );
521
522 let current_custom_names =
524 extractor.extract_dependency_custom_names(current_resource).await.with_context(|| {
525 format!(
526 "Failed to extract custom dependency names from resource '{}' (type: {:?})",
527 current_resource.name, current_resource.resource_type
528 )
529 })?;
530 tracing::debug!(
531 "Extracted {} custom names from current resource '{}' (type: {:?})",
532 current_custom_names.len(),
533 current_resource.name,
534 current_resource.resource_type
535 );
536 if !current_custom_names.is_empty() || current_resource.name.contains("golang") {
537 tracing::debug!(
538 "Extracted {} custom names from current resource '{}' (type: {:?})",
539 current_custom_names.len(),
540 current_resource.name,
541 current_resource.resource_type
542 );
543 for (dep_ref, custom_name) in ¤t_custom_names {
544 tracing::debug!(" Will add alias: '{}' -> '{}'", dep_ref, custom_name);
545 }
546 }
547 for (dep_ref, custom_name) in current_custom_names {
548 add_custom_alias(&mut deps, &dep_ref, &custom_name);
549 }
550
551 tracing::debug!(
553 "Built dependencies data with {} resource types for '{}'",
554 deps.len(),
555 current_resource.name
556 );
557 for (resource_type, resources) in &deps {
558 tracing::debug!(" Type {}: {} resources", resource_type, resources.len());
559 for (key, data) in resources {
560 if resource_type == "snippets" || data.name.contains("frontend-engineer") {
561 tracing::debug!(
562 " [CONTEXT-{}] For '{}': key='{}', name='{}', path='{}'",
563 resource_type,
564 current_resource.name,
565 key,
566 data.name,
567 data.path
568 );
569 } else {
570 tracing::debug!(" - {}", key);
571 }
572 }
573 }
574
575 Ok(deps)
576}
577
578pub(crate) fn add_custom_alias(
588 deps: &mut BTreeMap<String, BTreeMap<String, DependencyData>>,
589 dep_ref: &str,
590 custom_name: &str,
591) {
592 let dep_ref_parsed = match LockfileDependencyRef::from_str(dep_ref) {
594 Ok(dep_ref) => dep_ref,
595 Err(e) => {
596 tracing::debug!(
597 "Skipping invalid dep_ref format '{}' for custom name '{}': {}",
598 dep_ref,
599 custom_name,
600 e
601 );
602 return;
603 }
604 };
605
606 let dep_type = dep_ref_parsed.resource_type;
607 let dep_name = &dep_ref_parsed.path;
608
609 let type_str_plural = dep_type.to_plural().to_string();
610
611 if let Some(type_deps) = deps.get_mut(&type_str_plural) {
613 let name_to_key: HashMap<&str, &String> = type_deps
615 .iter()
616 .flat_map(|(key, data)| {
617 let mut mappings = vec![(data.name.as_str(), key)];
619
620 if let Some(basename) = Path::new(&data.name).file_name().and_then(|n| n.to_str()) {
622 mappings.push((basename, key));
623 }
624 if let Some(stem) = Path::new(&data.path).file_stem().and_then(|n| n.to_str()) {
625 mappings.push((stem, key));
626 }
627 if let Some(path_basename) =
628 Path::new(&data.path).file_name().and_then(|n| n.to_str())
629 {
630 mappings.push((path_basename, key));
631 }
632
633 mappings
634 })
635 .collect();
636
637 let existing_data = name_to_key
639 .get(dep_name.as_str())
640 .and_then(|key| type_deps.get(*key).cloned())
641 .or_else(|| {
642 Path::new(dep_name.as_str())
646 .file_name()
647 .and_then(|name| name.to_str())
648 .and_then(|basename| name_to_key.get(basename))
649 .and_then(|key| type_deps.get(*key).cloned())
650 });
651
652 if let Some(data) = existing_data {
653 let sanitized_alias = custom_name.replace('-', "_");
655
656 tracing::debug!(
657 "ā Added {} alias '{}' -> resource '{}' (path: {})",
658 type_str_plural,
659 sanitized_alias,
660 dep_name,
661 data.path
662 );
663
664 type_deps.insert(sanitized_alias.clone(), data);
666 } else {
667 if tracing::enabled!(tracing::Level::ERROR) {
669 let available_keys = type_deps
670 .iter()
671 .take(5)
672 .map(|(k, v)| format!("'{}' (name='{}')", k, v.name))
673 .collect::<Vec<_>>()
674 .join(", ");
675
676 tracing::error!(
677 "ā NOT FOUND: {} resource '{}' for alias '{}'.\n \
678 Dep ref: '{}'\n \
679 Available {} (first 5): {}",
680 type_str_plural,
681 dep_name,
682 custom_name,
683 dep_ref,
684 type_deps.len(),
685 available_keys
686 );
687 }
688 }
689 } else {
690 tracing::debug!(
691 "Resource type '{}' not found in deps map when adding custom alias '{}' for '{}'",
692 type_str_plural,
693 custom_name,
694 dep_ref
695 );
696 }
697}