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