Skip to main content

helm_schema_ir/contract/
graph.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::contract::FinalizedContract;
4use crate::contract_normalization::{
5    canonicalize_contract_uses, drop_default_guard_subsumed_duplicates,
6    drop_self_truthy_subsumed_duplicates, normalize_contract_uses,
7};
8use crate::observed_facts::{
9    ActivatedValuesDefaultSource, ActivatedValuesRootOverlay, HintGrade, ObservedFacts,
10};
11use crate::{ContractUse, Guard, ValueKind, YamlPath};
12
13/// Opaque guarded contract graph for one template interpretation.
14///
15/// Accumulation, path rebasing, and normalization live behind this
16/// contract-layer artifact instead of a raw vector owned by callers.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct ContractIr {
19    uses: Vec<ContractUse>,
20    dependency_uses: Vec<ContractUse>,
21    observed_facts: ObservedFacts,
22    values_program_wrappers: BTreeSet<helm_schema_core::ValuesProgramWrapper>,
23    /// Values paths whose nodes must NOT gain a wrapper alternative: a
24    /// strict string consumer reads them BEFORE the engine's values-root
25    /// rewrite, so a wrapper map there aborts rendering (nats'
26    /// `nameOverride` through `fullname | trunc`).
27    values_program_wrapper_exclusions: BTreeSet<helm_schema_core::ValuesPath>,
28    dependency_values_root_fragments: BTreeSet<String>,
29}
30
31impl ContractIr {
32    /// Build a contract graph from already-structured contract claims.
33    ///
34    /// This is the contract-layer constructor for tests and expert callers
35    /// that already have semantic claims. Schema signals are still derived
36    /// through [`ContractIr::finalize`], so semantic finalization stays on
37    /// the contract graph rather than a serialized document.
38    #[must_use]
39    pub fn from_contract_uses(uses: Vec<ContractUse>) -> Self {
40        Self {
41            uses,
42            ..Self::default()
43        }
44    }
45
46    pub(crate) fn push(&mut self, contract_use: ContractUse) {
47        self.uses.push(contract_use);
48    }
49
50    pub(crate) fn push_dependency_use(&mut self, contract_use: ContractUse) {
51        self.dependency_uses.push(contract_use);
52    }
53
54    /// Add a pathless scalar claim for a value path.
55    ///
56    /// Pathless claims make a value path visible to downstream schema
57    /// generation without asserting any rendered Kubernetes field shape.
58    pub fn push_pathless_scalar(&mut self, source_expr: impl Into<String>) {
59        let source_expr = source_expr.into();
60        self.push(ContractUse::new(
61            helm_schema_core::ValuesPath::parse(&source_expr),
62            YamlPath(Vec::new()),
63            ValueKind::Scalar,
64            Vec::new(),
65            None,
66        ));
67    }
68
69    /// Records a pathless fragment accepted at a dependency values root.
70    pub fn push_pathless_dependency_fragment(&mut self, source_expr: impl Into<String>) {
71        self.dependency_values_root_fragments
72            .insert(source_expr.into());
73    }
74
75    /// Move all claims from another contract graph into this graph.
76    pub fn append(&mut self, mut other: Self) {
77        self.uses.append(&mut other.uses);
78        self.dependency_uses.append(&mut other.dependency_uses);
79        self.dependency_values_root_fragments
80            .append(&mut other.dependency_values_root_fragments);
81        self.observed_facts.absorb(&other.observed_facts);
82        self.values_program_wrappers
83            .append(&mut other.values_program_wrappers);
84        self.values_program_wrapper_exclusions
85            .append(&mut other.values_program_wrapper_exclusions);
86    }
87
88    /// Record that rendering FAILS whenever `condition` holds — an
89    /// unconditionally reached `include` whose helper only an inactive
90    /// optional dependency defines aborts with "no template". The predicate
91    /// lowers through the standard terminal-clause machinery.
92    pub fn add_terminal_fail_condition(&mut self, condition: helm_schema_core::Predicate) {
93        let capture = crate::eval_effect::FailCapture {
94            conjunction: vec![condition],
95            ranged: crate::range_modes::RangeModes::default(),
96            kind: crate::eval_effect::CaptureKind::Fail,
97        };
98        self.observed_facts.captures.insert(capture);
99    }
100
101    /// Append guards to every claim in the graph without rewriting any paths.
102    ///
103    /// This is used for chart-structural activation predicates that apply to
104    /// an already-scoped batch of claims, such as dependency `condition:` /
105    /// `tags:` liveness from `Chart.yaml`.
106    pub fn append_guards_to_all_uses(&mut self, guards: &[Guard]) {
107        for contract_use in self.uses.iter_mut().chain(&mut self.dependency_uses) {
108            contract_use.condition = contract_use
109                .condition
110                .conjoined_with_guards(guards.iter().cloned());
111        }
112        // Fail captures are claims too: a `fail` inside a dependency gated
113        // off by `condition:` / `tags:` cannot abort rendering, so its
114        // conjunction must carry the activation predicate like every row.
115        self.observed_facts.captures = std::mem::take(&mut self.observed_facts.captures)
116            .into_iter()
117            .map(|mut capture| {
118                capture.conjunction.splice(
119                    0..0,
120                    guards
121                        .iter()
122                        .cloned()
123                        .map(helm_schema_core::Predicate::from),
124                );
125                capture
126            })
127            .collect();
128        if !guards.is_empty() {
129            let values_default_sources =
130                std::mem::take(&mut self.observed_facts.values_default_sources)
131                    .into_iter()
132                    .filter(|source| {
133                        self.uses.iter().chain(&self.dependency_uses).any(|use_| {
134                            !use_.path.0.is_empty()
135                                && use_.source_expr != source.target_path
136                                && use_.source_expr != source.source_path
137                                && !use_.source_expr.is_descendant_of(&source.source_path)
138                        })
139                    })
140                    .collect::<Vec<_>>();
141            let append_guards = |mut existing: Vec<Guard>| {
142                existing.extend(guards.iter().cloned());
143                existing.sort();
144                existing.dedup();
145                existing
146            };
147            self.observed_facts.activated_values_default_sources =
148                std::mem::take(&mut self.observed_facts.activated_values_default_sources)
149                    .into_iter()
150                    .map(|fact| ActivatedValuesDefaultSource {
151                        guards: append_guards(fact.guards),
152                        source: fact.source,
153                    })
154                    .chain(values_default_sources.into_iter().map(|source| {
155                        ActivatedValuesDefaultSource {
156                            guards: append_guards(Vec::new()),
157                            source,
158                        }
159                    }))
160                    .collect();
161            self.observed_facts.activated_values_root_overlays =
162                std::mem::take(&mut self.observed_facts.activated_values_root_overlays)
163                    .into_iter()
164                    .map(|fact| ActivatedValuesRootOverlay {
165                        guards: append_guards(fact.guards),
166                        target_path: fact.target_path,
167                        source_path: fact.source_path,
168                    })
169                    .chain(
170                        std::mem::take(&mut self.observed_facts.values_root_overlays)
171                            .into_iter()
172                            .map(|fact| ActivatedValuesRootOverlay {
173                                guards: append_guards(Vec::new()),
174                                target_path: fact.target_path,
175                                source_path: fact.source_path,
176                            }),
177                    )
178                    .collect();
179        }
180    }
181
182    /// Mark rendered claims as textual output rather than structured YAML
183    /// placements. Runtime operand contracts and terminal effects remain
184    /// unchanged.
185    pub fn mark_rendered_output_textual(&mut self) {
186        for contract_use in self.uses.iter_mut().chain(&mut self.dependency_uses) {
187            contract_use.kind = ValueKind::Serialized;
188        }
189    }
190
191    /// Rewrite all referenced values paths while preserving rendered YAML paths.
192    ///
193    /// This is used at chart boundaries where a dependency's `.Values.foo`
194    /// contract becomes `.Values.subchart.foo`, while rendered manifest paths
195    /// such as `metadata.name` stay unchanged.
196    pub fn map_value_paths<F>(&mut self, mut map: F)
197    where
198        F: FnMut(helm_schema_core::ValuesPath) -> helm_schema_core::ValuesPath,
199    {
200        let Self {
201            uses,
202            dependency_uses,
203            observed_facts,
204            values_program_wrappers,
205            values_program_wrapper_exclusions,
206            dependency_values_root_fragments,
207        } = self;
208        for contract_use in uses.iter_mut().chain(dependency_uses) {
209            contract_use.map_value_paths(&mut map);
210        }
211        *dependency_values_root_fragments = std::mem::take(dependency_values_root_fragments)
212            .into_iter()
213            .map(|path| map(helm_schema_core::ValuesPath::parse(&path)).encode())
214            .collect();
215        observed_facts.map_value_paths(&mut map);
216        *values_program_wrappers = std::mem::take(values_program_wrappers)
217            .into_iter()
218            .map(|wrapper| helm_schema_core::ValuesProgramWrapper {
219                scope_path: map(wrapper.scope_path),
220                key: wrapper.key,
221                spread: wrapper.spread,
222            })
223            .collect();
224        *values_program_wrapper_exclusions = std::mem::take(values_program_wrapper_exclusions)
225            .into_iter()
226            .map(&mut map)
227            .collect();
228    }
229
230    /// Projects dependency `global.*` contracts through Helm's parent-first
231    /// coalesce while preserving the dependency-local fallback.
232    pub fn project_dependency_global_contracts(&mut self, prefix: &[String]) {
233        if prefix.is_empty() {
234            return;
235        }
236        let global_sources = dependency_global_sources(prefix);
237        project_global_uses(&mut self.uses, &global_sources);
238        project_global_uses(&mut self.dependency_uses, &global_sources);
239        project_global_fail_captures(&mut self.observed_facts.captures, &global_sources);
240        project_global_range_modes(&mut self.observed_facts.range_modes, &global_sources);
241    }
242
243    /// Add declared input-type hints for values paths without projecting them
244    /// as inspection rows.
245    pub fn add_type_hint(&mut self, path: impl Into<String>, schema_type: impl Into<String>) {
246        let path = path.into();
247        let schema_type = schema_type.into();
248        if path.trim().is_empty() || schema_type.trim().is_empty() {
249            return;
250        }
251        self.observed_facts.insert_type_hint(
252            HintGrade::DECLARED,
253            helm_schema_core::ValuesPath::parse(&path),
254            &schema_type,
255        );
256    }
257
258    pub(crate) fn absorb_observed_facts(&mut self, facts: &ObservedFacts) {
259        self.observed_facts.absorb(facts);
260    }
261
262    pub(crate) fn extend_values_program_wrappers(
263        &mut self,
264        wrappers: impl IntoIterator<Item = helm_schema_core::ValuesProgramWrapper>,
265    ) {
266        self.values_program_wrappers.extend(wrappers);
267    }
268
269    pub(crate) fn extend_values_program_wrapper_exclusions(
270        &mut self,
271        paths: impl IntoIterator<Item = helm_schema_core::ValuesPath>,
272    ) {
273        self.values_program_wrapper_exclusions.extend(paths);
274    }
275
276    /// Drop evidence recorded AT a program-wrapper sentinel key: within a
277    /// wrapper-engine chart a `$tplYaml`-keyed member is the engine's own
278    /// dispatch convention — probed by the recursive walker, replaced
279    /// before ordinary consumers read the tree — never an ordinary chart
280    /// value, so reads and fail predicates over such paths must not mint
281    /// values properties. The wrapper alternatives model those nodes.
282    pub(crate) fn scrub_program_wrapper_sentinel_evidence(&mut self) {
283        let keys: std::collections::BTreeSet<String> = self
284            .values_program_wrappers
285            .iter()
286            .map(|wrapper| wrapper.key.clone())
287            .collect();
288        if keys.is_empty() {
289            return;
290        }
291        let touches = |path: &helm_schema_core::ValuesPath| {
292            path.segments()
293                .filter_map(helm_schema_core::Segment::literal)
294                .any(|segment| keys.contains(segment))
295        };
296        self.uses
297            .retain(|contract_use| !touches(&contract_use.source_expr));
298        self.dependency_uses
299            .retain(|contract_use| !touches(&contract_use.source_expr));
300        self.observed_facts.captures.retain(|capture| {
301            let mut paths: Vec<helm_schema_core::ValuesPath> = capture
302                .conjunction
303                .iter()
304                .flat_map(helm_schema_core::Predicate::value_paths)
305                .collect();
306            let mut kind = capture.kind.clone();
307            kind.map_value_paths(&mut |path| {
308                paths.push(path.clone());
309                path
310            });
311            !paths.iter().any(touches)
312        });
313    }
314
315    /// Finalize the contract once and derive downstream artifacts from that
316    /// one normalized contract representation.
317    #[must_use]
318    #[tracing::instrument(skip_all)]
319    pub fn finalize(mut self) -> FinalizedContract {
320        self.scrub_program_wrapper_sentinel_evidence();
321        let Self {
322            mut uses,
323            mut dependency_uses,
324            observed_facts,
325            values_program_wrappers,
326            values_program_wrapper_exclusions,
327            dependency_values_root_fragments,
328        } = self;
329        for source_expr in &dependency_values_root_fragments {
330            dependency_uses.push(ContractUse::new(
331                helm_schema_core::ValuesPath::parse(source_expr),
332                YamlPath(Vec::new()),
333                ValueKind::Fragment,
334                Vec::new(),
335                None,
336            ));
337        }
338        normalize_contract_uses(&mut uses);
339        drop_self_truthy_subsumed_duplicates(&mut dependency_uses);
340        canonicalize_contract_uses(&mut dependency_uses);
341        uses.append(&mut dependency_uses);
342        drop_default_guard_subsumed_duplicates(&mut uses);
343        drop_self_truthy_subsumed_duplicates(&mut uses);
344        canonicalize_contract_uses(&mut uses);
345        let fail_conditions = observed_facts.captures.iter().cloned().collect::<Vec<_>>();
346        lower_string_requirement_merge_sources(&mut uses, &fail_conditions);
347        canonicalize_contract_uses(&mut uses);
348        FinalizedContract::new(
349            uses,
350            &observed_facts,
351            values_program_wrappers,
352            values_program_wrapper_exclusions,
353            &dependency_values_root_fragments,
354        )
355    }
356}
357
358fn string_requirements_by_ancestor(
359    fail_conditions: &[crate::eval_effect::FailCapture],
360) -> BTreeMap<String, BTreeSet<(String, Vec<helm_schema_core::Predicate>)>> {
361    let mut requirements: BTreeMap<String, BTreeSet<(String, Vec<helm_schema_core::Predicate>)>> =
362        BTreeMap::new();
363    for capture in fail_conditions {
364        let crate::eval_effect::CaptureKind::StringRequirement {
365            path,
366            route,
367            selection,
368        } = &capture.kind
369        else {
370            continue;
371        };
372        if *route == crate::eval_effect::StringRequirementRoute::Serialized {
373            continue;
374        }
375        let mut predicates = capture.conjunction.clone();
376        predicates.extend(selection.iter().cloned());
377        predicates.sort();
378        predicates.dedup();
379        let segments = path
380            .segments()
381            .map(helm_schema_core::Segment::encode_component)
382            .collect::<Vec<_>>();
383        for end in 1..segments.len() {
384            requirements
385                .entry(helm_schema_core::join_encoded_value_path(
386                    segments.get(..end).unwrap_or_default().iter().cloned(),
387                ))
388                .or_default()
389                .insert((path.encode(), predicates.clone()));
390        }
391    }
392    requirements
393}
394
395fn lower_string_requirement_merge_sources(
396    uses: &mut [ContractUse],
397    fail_conditions: &[crate::eval_effect::FailCapture],
398) {
399    let requirements_by_ancestor = string_requirements_by_ancestor(fail_conditions);
400
401    let mut overlap_cache = BTreeMap::new();
402
403    for contract_use in uses.iter_mut() {
404        let Some(merge) = &contract_use.merge_layers else {
405            continue;
406        };
407        let source_expr = contract_use.source_expr.encode();
408        let Some(requirements) = requirements_by_ancestor.get(&source_expr) else {
409            continue;
410        };
411        let source_segments = contract_use
412            .source_expr
413            .segments()
414            .map(helm_schema_core::Segment::encode_component)
415            .collect::<Vec<_>>();
416        let specific_layers = merge
417            .layers()
418            .iter()
419            .map(|layer| {
420                layer
421                    .path
422                    .segments()
423                    .map(helm_schema_core::Segment::encode_component)
424                    .collect::<Vec<_>>()
425            })
426            .filter(|layer| {
427                layer.len() > source_segments.len() && layer.starts_with(source_segments.as_slice())
428            })
429            .collect::<Vec<_>>();
430        let [first, second, rest @ ..] = specific_layers.as_slice() else {
431            continue;
432        };
433        let mut common_suffix_len = first
434            .iter()
435            .rev()
436            .zip(second.iter().rev())
437            .take_while(|(left, right)| left == right)
438            .count();
439        for layer in rest {
440            common_suffix_len = common_suffix_len.min(
441                first
442                    .iter()
443                    .rev()
444                    .zip(layer.iter().rev())
445                    .take_while(|(left, right)| left == right)
446                    .count(),
447            );
448        }
449        let suffix = first.get(first.len().saturating_sub(common_suffix_len)..);
450        let Some(suffix) = suffix.filter(|suffix| {
451            !suffix.is_empty() && suffix.iter().all(|segment| segment.as_str() != "*")
452        }) else {
453            continue;
454        };
455        let Some(requirements) =
456            merge_suffix_string_requirements(&source_expr, requirements, suffix)
457        else {
458            continue;
459        };
460        let cache_key = (
461            contract_use.source_expr.clone(),
462            suffix.to_vec(),
463            contract_use.condition.clone(),
464        );
465        let overlaps_requirement = overlap_cache.get(&cache_key).copied().unwrap_or_else(|| {
466            let overlaps = contract_use.condition.disjuncts().iter().any(|row| {
467                requirements.iter().any(|requirement| {
468                    !helm_schema_core::GuardDnf::from_conjunction(
469                        row.iter().cloned().chain(requirement.iter().cloned()),
470                    )
471                    .is_never()
472                })
473            });
474            overlap_cache.insert(cache_key, overlaps);
475            overlaps
476        });
477        if !overlaps_requirement {
478            continue;
479        }
480        let source = helm_schema_core::ValuesPath::parse(&source_expr);
481        let projected =
482            helm_schema_core::ValuesPath::parse(&helm_schema_core::join_encoded_value_path(
483                source_segments
484                    .iter()
485                    .cloned()
486                    .chain(suffix.iter().cloned()),
487            ));
488        contract_use.map_value_paths(&mut |path| {
489            if path == source {
490                projected.clone()
491            } else {
492                path
493            }
494        });
495    }
496}
497
498fn merge_suffix_string_requirements(
499    source: &str,
500    requirements: &BTreeSet<(String, Vec<helm_schema_core::Predicate>)>,
501    suffix: &[String],
502) -> Option<BTreeSet<Vec<helm_schema_core::Predicate>>> {
503    let source_segments = helm_schema_core::split_value_path(source);
504    let mut paths = BTreeSet::new();
505    let mut conjunctions = BTreeSet::new();
506    for (path, predicates) in requirements {
507        let segments = helm_schema_core::split_value_path(path);
508        if segments.len() <= source_segments.len()
509            || !segments.starts_with(source_segments.as_slice())
510            || !segments.ends_with(suffix)
511        {
512            continue;
513        }
514        paths.insert(segments);
515        conjunctions.insert(predicates.clone());
516    }
517    (paths.len() >= 2).then_some(conjunctions)
518}
519
520fn dependency_global_sources(prefix: &[String]) -> Vec<String> {
521    (0..=prefix.len())
522        .map(|prefix_len| {
523            helm_schema_core::join_value_path(
524                prefix
525                    .get(..prefix_len)
526                    .unwrap_or_default()
527                    .iter()
528                    .cloned()
529                    .chain(std::iter::once("global".to_string())),
530            )
531        })
532        .collect()
533}
534
535fn project_global_uses(uses: &mut Vec<ContractUse>, global_sources: &[String]) {
536    let Some(dependency_global) = global_sources.last() else {
537        return;
538    };
539    let dependency_global_segments = helm_schema_core::split_value_path(dependency_global);
540    let mut projected = Vec::with_capacity(uses.len());
541    for dependency_use in std::mem::take(uses) {
542        let source_segments = dependency_use
543            .source_expr
544            .segments()
545            .map(helm_schema_core::Segment::encode_component)
546            .collect::<Vec<_>>();
547        let Some(relative) = source_segments.strip_prefix(dependency_global_segments.as_slice())
548        else {
549            projected.push(dependency_use);
550            continue;
551        };
552        let Some((selection_relative, key)) = global_selection_path(relative) else {
553            projected.push(dependency_use);
554            continue;
555        };
556
557        for (source_index, global_source) in global_sources.iter().enumerate() {
558            let selection_source = helm_schema_core::join_encoded_value_path(
559                helm_schema_core::split_value_path(global_source)
560                    .into_iter()
561                    .chain(selection_relative.iter().cloned()),
562            );
563            let mut selected_use = dependency_use.clone();
564            selected_use.map_value_paths(&mut |path| {
565                replace_value_path_prefix(path, dependency_global, global_source)
566            });
567            selected_use.condition =
568                selected_use
569                    .condition
570                    .conjoined_with_guards(global_source_selection_guards(
571                        global_sources,
572                        selection_relative,
573                        source_index,
574                        &selection_source,
575                        key,
576                    ));
577            projected.push(selected_use);
578        }
579    }
580    *uses = projected;
581}
582
583fn global_selection_path(relative: &[String]) -> Option<(&[String], &str)> {
584    let selection_len = relative
585        .iter()
586        .position(|segment| segment == "*")
587        .unwrap_or(relative.len());
588    let selection = relative.get(..selection_len)?;
589    Some((selection, selection.last()?.as_str()))
590}
591
592fn global_source_selection_guards(
593    global_sources: &[String],
594    relative: &[String],
595    source_index: usize,
596    source: &str,
597    key: &str,
598) -> Vec<Guard> {
599    let mut guards = Vec::new();
600    for higher_priority in global_sources.iter().take(source_index) {
601        let higher_source = helm_schema_core::join_encoded_value_path(
602            helm_schema_core::split_value_path(higher_priority)
603                .into_iter()
604                .chain(relative.iter().cloned()),
605        );
606        let mut container_segments = helm_schema_core::split_value_path(&higher_source);
607        container_segments.pop();
608        guards.push(Guard::AnyOf {
609            alternatives: vec![
610                vec![Guard::NotHasKey {
611                    path: helm_schema_core::ValuesPath::parse(
612                        &helm_schema_core::join_encoded_value_path(container_segments),
613                    ),
614                    key: key.to_string(),
615                }],
616                vec![Guard::Eq {
617                    path: helm_schema_core::ValuesPath::parse(&higher_source),
618                    value: helm_schema_core::GuardValue::Null,
619                }],
620            ],
621        });
622    }
623    if source_index + 1 < global_sources.len() {
624        let mut container_segments = helm_schema_core::split_value_path(source);
625        container_segments.pop();
626        guards.push(Guard::HasKey {
627            path: helm_schema_core::ValuesPath::parse(&helm_schema_core::join_encoded_value_path(
628                container_segments,
629            )),
630            key: key.to_string(),
631        });
632        guards.push(Guard::NotEq {
633            path: helm_schema_core::ValuesPath::parse(source),
634            value: helm_schema_core::GuardValue::Null,
635        });
636    }
637    guards
638}
639
640fn replace_value_path_prefix(
641    path: helm_schema_core::ValuesPath,
642    from: &str,
643    to: &str,
644) -> helm_schema_core::ValuesPath {
645    let from = helm_schema_core::ValuesPath::parse(from);
646    if path != from && !path.is_descendant_of(&from) {
647        return path;
648    }
649    let relative = path
650        .segments()
651        .skip(from.segments().len())
652        .cloned()
653        .collect::<Vec<_>>();
654    let target = helm_schema_core::ValuesPath::parse(to);
655    helm_schema_core::ValuesPath::from_segments(target.segments().chain(&relative))
656}
657
658fn project_global_fail_captures(
659    captures: &mut BTreeSet<crate::eval_effect::FailCapture>,
660    global_sources: &[String],
661) {
662    let Some(dependency_global) = global_sources.last() else {
663        return;
664    };
665    let dependency_global_path = helm_schema_core::ValuesPath::parse(dependency_global);
666    let mut projected = BTreeSet::new();
667    for dependency_capture in std::mem::take(captures) {
668        let Some(source_path) = dependency_capture.kind.sole_value_path() else {
669            projected.insert(dependency_capture);
670            continue;
671        };
672        if source_path != &dependency_global_path
673            && !source_path.is_descendant_of(&dependency_global_path)
674        {
675            projected.insert(dependency_capture);
676            continue;
677        }
678        let relative = source_path
679            .segments()
680            .skip(dependency_global_path.segments().len())
681            .map(helm_schema_core::Segment::encode_component)
682            .collect::<Vec<_>>();
683        if relative.is_empty() {
684            if matches!(
685                dependency_capture.kind,
686                crate::eval_effect::CaptureKind::RangeInput { .. }
687            ) {
688                for global_source in global_sources {
689                    let mut selected_capture = dependency_capture.clone();
690                    selected_capture.conjunction = selected_capture
691                        .conjunction
692                        .into_iter()
693                        .map(|predicate| {
694                            predicate.map_value_paths(&mut |path| {
695                                replace_value_path_prefix(path, dependency_global, global_source)
696                            })
697                        })
698                        .collect();
699                    selected_capture.kind.map_value_paths(&mut |path| {
700                        replace_value_path_prefix(path, dependency_global, global_source)
701                    });
702                    projected.insert(selected_capture);
703                }
704            } else {
705                projected.insert(dependency_capture);
706            }
707            continue;
708        }
709        let Some((selection_relative, key)) = global_selection_path(&relative) else {
710            projected.insert(dependency_capture);
711            continue;
712        };
713
714        for (source_index, global_source) in global_sources.iter().enumerate() {
715            let selection_source = helm_schema_core::join_encoded_value_path(
716                helm_schema_core::split_value_path(global_source)
717                    .into_iter()
718                    .chain(selection_relative.iter().cloned()),
719            );
720            let mut selected_capture = dependency_capture.clone();
721            selected_capture.conjunction = selected_capture
722                .conjunction
723                .into_iter()
724                .map(|predicate| {
725                    predicate.map_value_paths(&mut |path| {
726                        replace_value_path_prefix(path, dependency_global, global_source)
727                    })
728                })
729                .collect();
730            selected_capture.kind.map_value_paths(&mut |path| {
731                replace_value_path_prefix(path, dependency_global, global_source)
732            });
733            selected_capture.conjunction.extend(
734                global_source_selection_guards(
735                    global_sources,
736                    selection_relative,
737                    source_index,
738                    &selection_source,
739                    key,
740                )
741                .into_iter()
742                .map(helm_schema_core::Predicate::from),
743            );
744            projected.insert(selected_capture);
745        }
746    }
747    *captures = projected;
748}
749
750fn project_global_range_modes(
751    range_modes: &mut crate::range_modes::RangeModes,
752    global_sources: &[String],
753) {
754    let Some(dependency_global) = global_sources.last() else {
755        return;
756    };
757    let dependency_path = helm_schema_core::ValuesPath::parse(dependency_global);
758    let dependency_segments = dependency_path
759        .segments()
760        .map(helm_schema_core::Segment::encode_component)
761        .collect::<Vec<_>>();
762    let projected_paths = range_modes
763        .iter()
764        .filter_map(|(path, mode)| {
765            let segments = path
766                .segments()
767                .map(helm_schema_core::Segment::encode_component)
768                .collect::<Vec<_>>();
769            let relative = segments.strip_prefix(dependency_segments.as_slice())?;
770            Some((path.clone(), relative.to_vec(), mode))
771        })
772        .collect::<Vec<_>>();
773
774    for (path, relative, mode) in projected_paths {
775        range_modes.remove(&path);
776        if relative.is_empty() {
777            for global_source in global_sources {
778                range_modes.merge_mode(&helm_schema_core::ValuesPath::parse(global_source), mode);
779            }
780            continue;
781        }
782        if global_selection_path(&relative).is_none() {
783            continue;
784        }
785        for global_source in global_sources {
786            let projected = helm_schema_core::join_encoded_value_path(
787                helm_schema_core::split_value_path(global_source)
788                    .into_iter()
789                    .chain(relative.iter().cloned()),
790            );
791            range_modes.merge_mode(&helm_schema_core::ValuesPath::parse(&projected), mode);
792        }
793    }
794}