Skip to main content

helm_schema_core/
contract_signals.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{Guard, GuardValue, Predicate, ProviderSchemaUse, ValuesPath};
4
5/// Values-decidable guard expression that can be lowered into JSON Schema
6/// conditionals.
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub enum ConditionalGuard {
9    /// The value at `path` is Helm-truthy.
10    Truthy {
11        /// Values path tested for truthiness.
12        path: ValuesPath,
13    },
14    /// A `with` action selected the non-empty value at `path`.
15    With {
16        /// Values path selected by the action.
17        path: ValuesPath,
18    },
19    /// The value at `path` equals a literal.
20    Eq {
21        /// Values path compared with the literal.
22        path: ValuesPath,
23        /// Literal required at the path.
24        value: GuardValue,
25    },
26    /// The value at `path` differs from a literal.
27    NotEq {
28        /// Values path compared with the literal.
29        path: ValuesPath,
30        /// Literal excluded at the path.
31        value: GuardValue,
32    },
33    /// The value at `path` is absent.
34    Absent {
35        /// Values path whose absence selects the branch.
36        path: ValuesPath,
37    },
38    /// The value at `path` has a specific JSON Schema type.
39    TypeIs {
40        /// Values path subjected to the type test.
41        path: ValuesPath,
42        /// JSON Schema type name accepted by the branch.
43        schema_type: String,
44    },
45    /// The string at `path` matches a regular expression.
46    MatchesPattern {
47        /// Values path subjected to the pattern test.
48        path: ValuesPath,
49        /// ECMA-compatible regular expression required by the branch.
50        pattern: String,
51    },
52    /// The path's RAW value is a JSON integer strictly greater than `bound`
53    /// — a sound SUBSET of the Sprig coercion (`gt (int64 x) bound`) it
54    /// stands in for, valid only where firing less often is safe.
55    IntGt {
56        /// Values path subjected to the integer comparison.
57        path: ValuesPath,
58        /// Exclusive lower bound.
59        bound: i64,
60    },
61    /// The mirror of [`ConditionalGuard::IntGt`]: the path's RAW value is a
62    /// JSON integer strictly less than `bound`, under the same sound-subset
63    /// contract.
64    IntLt {
65        /// Values path subjected to the integer comparison.
66        path: ValuesPath,
67        /// Exclusive upper bound.
68        bound: i64,
69    },
70    /// The mapping at `path` contains the literal member `key`. The key is
71    /// an OPAQUE property name (it may contain dots), so it rides beside
72    /// the segmented path instead of being appended to it.
73    HasKey {
74        /// Values path expected to hold a mapping.
75        path: ValuesPath,
76        /// Literal mapping key whose presence selects the branch.
77        key: String,
78    },
79    /// SOME iterated item of the collection at `path` has `member` equal to
80    /// `value` — the document-level meaning of a range-sentinel flag
81    /// (`Range(path) ∧ Eq(path.*.member, value)`). Lowers to `contains`
82    /// over the array lane and the double-negated member quantifier over
83    /// the object lane.
84    ContainsMemberEquals {
85        /// Values path expected to hold the iterated collection.
86        path: ValuesPath,
87        /// Member name compared within each collection item.
88        member: String,
89        /// Literal that at least one member must equal.
90        value: GuardValue,
91    },
92    /// SOME iterated item of the collection at `path` has a Helm-truthy
93    /// `member` — the document-level meaning of a Boolean range sentinel
94    /// (`Range(path) ∧ Truthy(path.*.member)`).
95    ContainsTruthyMember {
96        /// Values path expected to hold the iterated collection.
97        path: ValuesPath,
98        /// Member whose truthiness selects the sentinel state.
99        member: String,
100    },
101    /// SOME item of the list at `path` deep-equals the scalar literal —
102    /// Sprig `has LITERAL .Values.list`. `has` returns false on a nil
103    /// haystack and aborts on non-lists, so the guard holds exactly for
104    /// arrays carrying the literal; lowers to `contains` with a `const`
105    /// item.
106    ContainsEquals {
107        /// Values path expected to hold the list.
108        path: ValuesPath,
109        /// Literal that at least one list item must equal.
110        value: GuardValue,
111    },
112    /// The collection at `path` has at most one entry — the document-level
113    /// form of "every iteration of this range is the first" (an
114    /// empty-initialized dedup accumulator cannot have shadowed anything).
115    /// A sound subset: it may only scope positive-polarity evidence.
116    AtMostOneMember {
117        /// Values path expected to hold the bounded collection.
118        path: ValuesPath,
119    },
120    /// The value at `path` is a mapping with at least `bound` members
121    /// (`gt (keys X | len) N`). Exact: both polarities encode.
122    MinMembers {
123        /// Values path expected to hold the mapping.
124        path: ValuesPath,
125        /// Inclusive minimum number of members.
126        bound: i64,
127    },
128    /// Logical negation of a guard.
129    Not(Box<ConditionalGuard>),
130    /// Conjunction of every enclosed guard.
131    AllOf(Vec<ConditionalGuard>),
132    /// Disjunction of the enclosed guards.
133    AnyOf(Vec<ConditionalGuard>),
134}
135
136impl ConditionalGuard {
137    /// Reports whether this guard tests the target's own Helm truthiness.
138    #[must_use]
139    pub fn is_self_truthy_for(&self, target: &ValuesPath) -> bool {
140        matches!(
141            self,
142            Self::Truthy { path } | Self::With { path }
143                if path == target
144        )
145    }
146
147    /// Reports whether this guard can hold only while the target path exists.
148    #[must_use]
149    pub fn is_self_presence_for(&self, target: &ValuesPath) -> bool {
150        match self {
151            Self::Not(inner) => {
152                matches!(inner.as_ref(), Self::Absent { path } if path == target)
153            }
154            Self::HasKey { path, key } => {
155                // The opaque member name must remain one structural segment.
156                let mut guarded = path.clone();
157                guarded.push(key.clone());
158                &guarded == target
159            }
160            _ => false,
161        }
162    }
163
164    /// Reconstructs the exact Boolean predicate represented by this schema guard.
165    #[must_use]
166    pub fn predicate(&self) -> Predicate {
167        match self {
168            Self::Truthy { path } => Predicate::from(Guard::Truthy { path: path.clone() }),
169            Self::With { path } => Predicate::from(Guard::With { path: path.clone() }),
170            Self::Eq { path, value } => Predicate::from(Guard::Eq {
171                path: path.clone(),
172                value: value.clone(),
173            }),
174            Self::NotEq { path, value } => Predicate::from(Guard::NotEq {
175                path: path.clone(),
176                value: value.clone(),
177            }),
178            Self::Absent { path } => Predicate::from(Guard::Absent { path: path.clone() }),
179            Self::TypeIs { path, schema_type } => Predicate::from(Guard::TypeIs {
180                path: path.clone(),
181                schema_type: schema_type.clone(),
182            }),
183            Self::MatchesPattern { path, pattern } => Predicate::from(Guard::MatchesPattern {
184                path: path.clone(),
185                pattern: pattern.clone(),
186                templated: false,
187            }),
188            Self::IntGt { path, bound } => Predicate::from(Guard::IntGt {
189                path: path.clone(),
190                bound: *bound,
191            }),
192            Self::IntLt { path, bound } => Predicate::from(Guard::IntLt {
193                path: path.clone(),
194                bound: *bound,
195            }),
196            Self::HasKey { path, key } => Predicate::from(Guard::HasKey {
197                path: path.clone(),
198                key: key.clone(),
199            }),
200            Self::ContainsMemberEquals {
201                path,
202                member,
203                value,
204            } => Predicate::from(Guard::ContainsMemberEquals {
205                path: path.clone(),
206                member: member.clone(),
207                value: value.clone(),
208            }),
209            Self::ContainsTruthyMember { path, member } => {
210                Predicate::from(Guard::ContainsTruthyMember {
211                    path: path.clone(),
212                    member: member.clone(),
213                })
214            }
215            Self::ContainsEquals { path, value } => Predicate::from(Guard::ContainsEquals {
216                path: path.clone(),
217                value: value.clone(),
218            }),
219            Self::AtMostOneMember { path } => {
220                Predicate::from(Guard::AtMostOneMember { path: path.clone() })
221            }
222            Self::MinMembers { path, bound } => Predicate::from(Guard::MinMembers {
223                path: path.clone(),
224                bound: *bound,
225            }),
226            Self::Not(inner) => inner.predicate().negated(),
227            Self::AllOf(guards) => Predicate::all(guards.iter().map(Self::predicate).collect()),
228            Self::AnyOf(guards) => Predicate::Or(guards.iter().map(Self::predicate).collect()),
229        }
230    }
231}
232
233impl TryFrom<&Guard> for ConditionalGuard {
234    type Error = ();
235
236    fn try_from(guard: &Guard) -> Result<Self, Self::Error> {
237        Ok(match guard {
238            Guard::Truthy { path } => Self::Truthy { path: path.clone() },
239            Guard::Not { path } => Self::Not(Box::new(Self::Truthy { path: path.clone() })),
240            Guard::Eq { path, value } => Self::Eq {
241                path: path.clone(),
242                value: value.clone(),
243            },
244            Guard::NotEq { path, value } => Self::NotEq {
245                path: path.clone(),
246                value: value.clone(),
247            },
248            Guard::Absent { path } => Self::Absent { path: path.clone() },
249            Guard::MatchesPattern {
250                path,
251                pattern,
252                templated: false,
253            } => Self::MatchesPattern {
254                path: path.clone(),
255                pattern: pattern.clone(),
256            },
257            Guard::Or { paths } => Self::AnyOf(
258                paths
259                    .iter()
260                    .map(|path| Self::Truthy { path: path.clone() })
261                    .collect(),
262            ),
263            Guard::AnyOf { alternatives } => Self::AnyOf(
264                alternatives
265                    .iter()
266                    .map(|guards| {
267                        guards
268                            .iter()
269                            .map(Self::try_from)
270                            .collect::<Result<Vec<_>, _>>()
271                            .map(Self::AllOf)
272                    })
273                    .collect::<Result<Vec<_>, _>>()?,
274            ),
275            Guard::With { path } => Self::With { path: path.clone() },
276            Guard::TypeIs { path, schema_type } => Self::TypeIs {
277                path: path.clone(),
278                schema_type: schema_type.clone(),
279            },
280            Guard::NotTypeIs { path, schema_type } => Self::Not(Box::new(Self::TypeIs {
281                path: path.clone(),
282                schema_type: schema_type.clone(),
283            })),
284            Guard::IntGt { path, bound } => Self::IntGt {
285                path: path.clone(),
286                bound: *bound,
287            },
288            Guard::IntLt { path, bound } => Self::IntLt {
289                path: path.clone(),
290                bound: *bound,
291            },
292            Guard::AtMostOneMember { path } => Self::AtMostOneMember { path: path.clone() },
293            Guard::MinMembers { path, bound } => Self::MinMembers {
294                path: path.clone(),
295                bound: *bound,
296            },
297            Guard::HasKey { path, key } => Self::HasKey {
298                path: path.clone(),
299                key: key.clone(),
300            },
301            Guard::NotHasKey { path, key } => Self::Not(Box::new(Self::HasKey {
302                path: path.clone(),
303                key: key.clone(),
304            })),
305            Guard::ContainsEquals { path, value } => Self::ContainsEquals {
306                path: path.clone(),
307                value: value.clone(),
308            },
309            Guard::ContainsMemberEquals {
310                path,
311                member,
312                value,
313            } => Self::ContainsMemberEquals {
314                path: path.clone(),
315                member: member.clone(),
316                value: value.clone(),
317            },
318            Guard::ContainsTruthyMember { path, member } => Self::ContainsTruthyMember {
319                path: path.clone(),
320                member: member.clone(),
321            },
322            Guard::MatchesPattern {
323                templated: true, ..
324            }
325            | Guard::NotMatchesPattern { .. }
326            | Guard::RangeKeyPrefix { .. }
327            | Guard::RangeKeyEquals { .. }
328            | Guard::RangeKeyMatches { .. }
329            | Guard::Range { .. }
330            | Guard::Default { .. } => return Err(()),
331        })
332    }
333}
334
335impl ConditionalGuard {
336    /// Returns every values path referenced by this guard tree.
337    #[must_use]
338    pub fn value_paths(&self) -> BTreeSet<ValuesPath> {
339        let mut paths = BTreeSet::new();
340        self.collect_value_paths(&mut paths);
341        paths
342    }
343
344    /// Rewrite the values paths carried by this guard (and every nested
345    /// guard).
346    #[must_use]
347    pub fn map_value_paths<F>(self, map: &mut F) -> Self
348    where
349        F: FnMut(ValuesPath) -> ValuesPath,
350    {
351        match self {
352            Self::Truthy { path } => Self::Truthy { path: map(path) },
353            Self::With { path } => Self::With { path: map(path) },
354            Self::Eq { path, value } => Self::Eq {
355                path: map(path),
356                value,
357            },
358            Self::NotEq { path, value } => Self::NotEq {
359                path: map(path),
360                value,
361            },
362            Self::Absent { path } => Self::Absent { path: map(path) },
363            Self::TypeIs { path, schema_type } => Self::TypeIs {
364                path: map(path),
365                schema_type,
366            },
367            Self::MatchesPattern { path, pattern } => Self::MatchesPattern {
368                path: map(path),
369                pattern,
370            },
371            Self::IntGt { path, bound } => Self::IntGt {
372                path: map(path),
373                bound,
374            },
375            Self::IntLt { path, bound } => Self::IntLt {
376                path: map(path),
377                bound,
378            },
379            Self::HasKey { path, key } => Self::HasKey {
380                path: map(path),
381                key,
382            },
383            Self::ContainsMemberEquals {
384                path,
385                member,
386                value,
387            } => Self::ContainsMemberEquals {
388                path: map(path),
389                member,
390                value,
391            },
392            Self::ContainsTruthyMember { path, member } => Self::ContainsTruthyMember {
393                path: map(path),
394                member,
395            },
396            Self::ContainsEquals { path, value } => Self::ContainsEquals {
397                path: map(path),
398                value,
399            },
400            Self::AtMostOneMember { path } => Self::AtMostOneMember { path: map(path) },
401            Self::MinMembers { path, bound } => Self::MinMembers {
402                path: map(path),
403                bound,
404            },
405            Self::Not(inner) => Self::Not(Box::new(inner.map_value_paths(map))),
406            Self::AllOf(guards) => Self::AllOf(
407                guards
408                    .into_iter()
409                    .map(|guard| guard.map_value_paths(map))
410                    .collect(),
411            ),
412            Self::AnyOf(guards) => Self::AnyOf(
413                guards
414                    .into_iter()
415                    .map(|guard| guard.map_value_paths(map))
416                    .collect(),
417            ),
418        }
419    }
420
421    fn collect_value_paths(&self, paths: &mut BTreeSet<ValuesPath>) {
422        match self {
423            Self::Truthy { path }
424            | Self::With { path }
425            | Self::Eq { path, .. }
426            | Self::NotEq { path, .. }
427            | Self::Absent { path }
428            | Self::TypeIs { path, .. }
429            | Self::MatchesPattern { path, .. }
430            | Self::IntGt { path, .. }
431            | Self::IntLt { path, .. }
432            | Self::HasKey { path, .. }
433            | Self::ContainsMemberEquals { path, .. }
434            | Self::ContainsTruthyMember { path, .. }
435            | Self::ContainsEquals { path, .. }
436            | Self::AtMostOneMember { path }
437            | Self::MinMembers { path, .. } => {
438                paths.insert(path.clone());
439            }
440            Self::Not(inner) => inner.collect_value_paths(paths),
441            Self::AllOf(guards) | Self::AnyOf(guards) => {
442                for guard in guards {
443                    guard.collect_value_paths(paths);
444                }
445            }
446        }
447    }
448}
449
450/// Semantic source of one conditional evidence branch.
451#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
452pub enum ConditionalOverlayFlavor {
453    /// Evidence selected by ordinary values control flow.
454    Ordinary,
455    /// Evidence tied to one producer-known Kubernetes kind alternative.
456    KindBranch,
457}
458
459/// Conditionally-scoped values path whose schema can be lowered under a
460/// values-decidable guard set.
461///
462/// Multiple entries in `guards` mean conjunction: all guards in the set must
463/// hold for the overlay to apply.
464#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
465pub struct ConditionalPathOverlay {
466    /// Conjoined conditions that select this overlay.
467    pub guards: Vec<ConditionalGuard>,
468    /// Schema evidence that applies while the guards hold.
469    pub evidence: ConditionalOverlayEvidence,
470    /// Keep the unconditional/base schema for this path alongside the guarded
471    /// overlay because the contract also observed an unguarded use.
472    pub preserve_base_schema: bool,
473    /// Semantic origin of the conditional evidence.
474    pub flavor: ConditionalOverlayFlavor,
475}
476
477/// Branch-local evidence for one conditional schema overlay.
478///
479/// The target path is implicit from the enclosing [`ContractPathSchemaEvidence`]
480/// entry that owns the overlay.
481#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
482pub struct ConditionalOverlayEvidence {
483    /// Behavioral facts observed in the selected branch.
484    pub facts: ContractValuePathFacts,
485    /// Runtime domain of a range that executes in this branch.
486    pub range_domain: Option<RangeDomain>,
487    /// Kubernetes metadata field roles reached in the branch.
488    pub metadata_field_kinds: BTreeSet<MetadataFieldKind>,
489    /// JSON Schema type names implied by branch-local consumers.
490    pub type_hints: BTreeSet<String>,
491    /// Resource-schema sinks reached in the selected branch.
492    pub provider_schema_uses: Vec<ProviderSchemaUse>,
493}
494
495impl ConditionalOverlayEvidence {
496    /// Materializes this branch-local evidence as path-local evidence.
497    #[must_use]
498    pub fn as_path_evidence(&self) -> ContractPathSchemaEvidence {
499        ContractPathSchemaEvidence {
500            is_referenced_value_path: true,
501            facts: self.facts,
502            guard_predicates: Vec::new(),
503            metadata_field_kinds: self.metadata_field_kinds.clone(),
504            type_hints: self.type_hints.clone(),
505            guarded_type_hints: BTreeSet::new(),
506            fallback_type_hints: BTreeSet::new(),
507            provider_schema_uses: self.provider_schema_uses.clone(),
508            requiredness: ContractRequirednessEvidence::default(),
509            conditional_overlays: Vec::new(),
510            requirement_implications: Vec::new(),
511        }
512    }
513}
514
515/// Runtime input kinds accepted by a range header in one conditional branch.
516#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
517pub enum RangeDomain {
518    /// Arrays, objects, and null; integer counts are not accepted.
519    CollectionOnly,
520    /// Arrays, objects, null, and Helm's integer-count input channel.
521    CollectionOrIntegerCount,
522}
523
524impl RangeDomain {
525    /// Whether this branch retains Helm's integer-count input channel.
526    #[must_use]
527    pub const fn allows_integer(self) -> bool {
528        match self {
529            Self::CollectionOnly => false,
530            Self::CollectionOrIntegerCount => true,
531        }
532    }
533}
534
535/// Kubernetes `metadata.*` field shape referenced by a values path.
536///
537/// The contract layer records the field category structurally from the
538/// rendered document path. JSON Schema lowering remains a generator policy.
539#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
540pub enum MetadataFieldKind {
541    /// `metadata.labels` and `metadata.annotations`.
542    StringMap,
543    /// `metadata.name`.
544    Name,
545    /// `metadata.namespace`.
546    Namespace,
547}
548
549/// All schema-lowering evidence for one values path.
550///
551/// The contract layer owns this view so downstream generation can consume one
552/// path-local static-analysis fact instead of reassembling meaning from
553/// several parallel maps.
554#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
555pub struct ContractPathSchemaEvidence {
556    /// Whether template analysis directly referenced this path.
557    pub is_referenced_value_path: bool,
558    /// Aggregate behavioral facts observed for the path.
559    pub facts: ContractValuePathFacts,
560    /// Unconditional guard facts attached to the path.
561    pub guard_predicates: Vec<ConditionalGuard>,
562    /// Kubernetes metadata field roles reached from the path.
563    pub metadata_field_kinds: BTreeSet<MetadataFieldKind>,
564    /// Unconditional JSON Schema type hints.
565    pub type_hints: BTreeSet<String>,
566    /// Hints observed only under branch predicates. At the path level these
567    /// may only WIDEN (add accepted alternatives to an otherwise-typed
568    /// base): `allOf` branches can narrow but never re-widen a base, so a
569    /// branch-scoped domain alternative must surface here.
570    pub guarded_type_hints: BTreeSet<String>,
571    /// Hints from literal `default`/`coalesce` fallbacks. The selection call
572    /// never consumes the raw value — every Helm-empty input takes the
573    /// fallback — so these type only the truthy arm and base lowering must
574    /// keep the whole Helm-falsy set open beside them.
575    pub fallback_type_hints: BTreeSet<String>,
576    /// Resource-schema sinks that consume the path.
577    pub provider_schema_uses: Vec<ProviderSchemaUse>,
578    /// Facts used by optional required-property inference.
579    pub requiredness: ContractRequirednessEvidence,
580    /// Branch-local evidence keyed by values-decidable guards.
581    pub conditional_overlays: Vec<ConditionalPathOverlay>,
582    /// Runtime-hard requirements that must hold wherever their outer guards
583    /// do. They may come from an explicit abort path or another producer that
584    /// proves the same rendering requirement, so lowering must not let weaker
585    /// evidence suppress them.
586    pub requirement_implications: Vec<ContractRequirementImplication>,
587}
588
589/// A chart-authored values-program wrapper convention: within `scope_path`
590/// (empty for the whole values tree), any node may be a singleton
591/// `{key: PROGRAM}` map that the chart's engine replaces with the
592/// `tpl`-rendered, YAML-reparsed program result before consumers read it.
593#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
594pub struct ValuesProgramWrapper {
595    /// Values subtree the engine rewrites; empty means the whole tree.
596    pub scope_path: crate::ValuesPath,
597    /// The wrapper's sentinel member key (`$tplYaml`).
598    pub key: String,
599    /// Whether the engine SPREADS the program result into the parent
600    /// collection instead of replacing the node (`$tplYamlSpread`): the
601    /// result's kind must match the parent's kind (a null result is a
602    /// no-op removal), and the values root itself rejects the wrapper.
603    pub spread: bool,
604}
605
606/// A chart-wide default subtree merged into an effective `.Values` subtree.
607///
608/// The target remains user-overridable; the source supplies only keys absent
609/// from the target, matching `mustMergeOverwrite SOURCE TARGET` before the
610/// result replaces a root or nested `Values` object.
611#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
612pub struct ValuesDefaultSource {
613    /// Effective values subtree receiving defaults, with an empty path denoting `.Values`.
614    pub target_path: crate::ValuesPath,
615    /// Chart values subtree supplying defaults.
616    pub source_path: crate::ValuesPath,
617}
618
619/// A chart-wide default source that executes only in one activation branch.
620#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
621pub struct GuardedValuesDefaultSource {
622    /// Chart activation conditions under which the runtime merge executes.
623    pub outer_guards: Vec<ConditionalGuard>,
624    /// Target/source pair applied while the guards hold.
625    pub source: ValuesDefaultSource,
626}
627
628/// One guarded runtime requirement on a values path.
629#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
630pub struct ContractRequirementImplication {
631    /// Conditions outside the requirement; empty means it binds the path
632    /// unconditionally.
633    pub outer_guards: Vec<ConditionalGuard>,
634    /// The runtime value affected by the requirement.
635    pub target: ContractRequirementTarget,
636    /// Conjunction of requirements the affected value must satisfy.
637    pub requirements: Vec<FailValueRequirement>,
638}
639
640/// Runtime value within a values-path contract that must satisfy a requirement.
641#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
642pub enum ContractRequirementTarget {
643    /// The values path itself.
644    Value,
645    /// Every value produced by ranging the path.
646    ///
647    /// `allow_integer` describes the range header's own integer lane. It is
648    /// false for a two-variable range, even when the member requirement would
649    /// otherwise accept integer values.
650    Members {
651        /// Whether Helm's integer-count range form remains accepted.
652        allow_integer: bool,
653    },
654    /// Every ranged member except named object entries supplied by a deeper
655    /// values layer. Array members and newly added object entries still bind.
656    MembersExceptKeys {
657        /// Object keys whose member requirement is satisfied after values
658        /// layering even when the parent layer omits the required leaf.
659        keys: BTreeSet<String>,
660        /// Whether Helm's integer-count range form remains accepted.
661        allow_integer: bool,
662    },
663    /// Values of object entries whose keys start with the literal prefix.
664    /// Empty arrays and null remain valid because they execute no range body.
665    MembersMatchingPrefix {
666        /// Literal key prefix selecting affected object entries.
667        prefix: String,
668    },
669    /// Each ranged member whose literal sibling equals `value` must satisfy
670    /// the requirements at `target_path`, both relative to that member.
671    MembersWhereEquals {
672        /// Relative member path used as the selector.
673        guard_path: Vec<String>,
674        /// Literal required at the selector path.
675        value: GuardValue,
676        /// Relative member path constrained by the requirement.
677        target_path: Vec<String>,
678    },
679    /// Every ranged member must CONTAIN `target_path` and its value there
680    /// must satisfy the requirements — an unconditional per-member field
681    /// read by a strict consumer (`tpl $member.url` fails on a missing or
682    /// non-string field). `allow_integer` mirrors [`Self::Members`].
683    MembersAt {
684        /// Relative member path that must exist.
685        target_path: Vec<String>,
686        /// Whether Helm's integer-count range form remains accepted.
687        allow_integer: bool,
688    },
689    /// [`Self::MembersAt`] restricted to the ranged members whose own field
690    /// at `guard_path` is Helm-truthy: the chart gates the read per member,
691    /// which only the member's own slot can express (the minio chart reads
692    /// `.existingSecretKey` inside `if .existingSecret`). The gate selects
693    /// WHICH members the requirements bind, so it never implies that the
694    /// target itself is present — that stays the absence claim's own fact.
695    MembersAtWhereTruthy {
696        /// Relative member path whose truthiness selects the members.
697        guard_path: Vec<String>,
698        /// Relative member path the requirements constrain; empty when they
699        /// constrain the selected member itself.
700        target_path: Vec<String>,
701        /// Whether Helm's integer-count range form remains accepted.
702        allow_integer: bool,
703    },
704    /// Every key produced by ranging the path.
705    Keys,
706}
707
708/// The quoting style of a manually quoted YAML scalar hosting a raw splice.
709#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
710pub enum QuotedScalarStyle {
711    /// `"…"` — every `\` must begin a YAML escape and every `"` be escaped.
712    Double,
713    /// `'…'` — `''` is the only escape, so every apostrophe must be doubled.
714    Single,
715}
716
717impl QuotedScalarStyle {
718    /// Valid CONTENT of a scalar quoted in this style; raw text outside the
719    /// grammar corrupts the manually quoted token.
720    #[must_use]
721    pub fn safe_content_pattern(self) -> &'static str {
722        match self {
723            Self::Double => {
724                r#"^([^"\\]|\\["\\/0abtnvfre N_LP]|\\x[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}|\\U[0-9A-Fa-f]{8})*$"#
725            }
726            Self::Single => r"^([^']|'')*$",
727        }
728    }
729}
730
731/// One requirement a `fail` branch imposes on an affected value.
732#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
733pub enum FailValueRequirement {
734    /// The value must be of this JSON Schema type.
735    SchemaType(String),
736    /// The value must be of this JSON Schema type EVEN WHEN NULL: the
737    /// consumer type-asserts before any nil check (Sprig `dig` subjects),
738    /// so an explicit null aborts while structural absence stays open
739    /// through the arm's properties anchoring.
740    SchemaTypeEvenNull(String),
741    /// The value must be of this JSON Schema type only when Helm-truthy:
742    /// every falsy spelling escapes through the consumer's own truthiness
743    /// selection (a ranged ACL member's `default ""` password reaching
744    /// `sha256sum` behind `if $password`).
745    TruthyImpliesSchemaType(String),
746    /// The value must be Helm-truthy (sealed-secrets aborts on any falsy
747    /// `privateKeyAnnotations` member, including the empty string).
748    HelmTruthy,
749    /// The value must be Helm-FALSY — the negation of a member's own
750    /// truthiness test inside a compound ranged terminal: the fail fires
751    /// only for truthy members, so falsiness is one escape alternative
752    /// (traefik's `if $config` gate around the http3-without-tls abort).
753    HelmFalsy,
754    /// The value's field at `path`, when present, must be Helm-FALSY: the
755    /// failing test fired on the field's truthiness (oauth2-proxy aborts
756    /// when a legacy `extraPaths[].backend.serviceName` is set under the
757    /// `networking.k8s.io/v1` Ingress api).
758    FieldHelmFalsy {
759        /// Relative field path constrained to Helm-falsy values.
760        path: Vec<String>,
761    },
762    /// The value must be an object whose field at `path` is present and
763    /// equals the literal: the failing test's negation held an equality on
764    /// the field (traefik's `eq $plugin.type "hostPath"` dispatch arm; Go's
765    /// `eq` aborts on a nil operand, so presence rides along).
766    FieldEquals {
767        /// Relative field path compared with the literal.
768        path: Vec<String>,
769        /// Literal required at the field path.
770        value: GuardValue,
771    },
772    /// The value must be an object whose field at `path` is present and
773    /// non-null: a ranged member's leaf renders into a provider-REQUIRED
774    /// resource field, where a missing or null source emits an explicit
775    /// null the provider rejects (promtail's extra Service `port`).
776    FieldPresentNotNull {
777        /// Relative field path that must contain a non-null value.
778        path: Vec<String>,
779    },
780    /// The value must be an object whose field at `path` is present and
781    /// Helm-truthy — the positive mirror of [`Self::FieldHelmFalsy`], used
782    /// as the ESCAPE alternative when a member-scoped branch guard selects
783    /// another render for truthy fields (promtail's `service` arm renders
784    /// its own port instead of `containerPort`).
785    FieldHelmTruthy {
786        /// Relative field path constrained to Helm-truthy values.
787        path: Vec<String>,
788    },
789    /// At least one alternative (each a conjunction of requirements) must
790    /// hold. A `fail` whose test conjoins several member conditions negates
791    /// to the DISJUNCTION of their negations — traefik's local plugins
792    /// render with a truthy `type` OR a legacy truthy `hostPath`, and
793    /// conjoining those requirements rejected both documented shapes.
794    AnyOf(Vec<Vec<FailValueRequirement>>),
795    /// The value must not equal this literal (cilium forbids ranged
796    /// `extraEnv` names colliding with its own backoff variables).
797    NotEquals(GuardValue),
798    /// The value's field at `path`, when present, must differ from the
799    /// literal — the negation of a member-field equality test. Absent and
800    /// null fields differ from every literal (Helm's `eq` compares `nil`
801    /// without aborting), so no presence requirement rides along
802    /// (traefik's HTTPS-protocol listeners must carry `certificateRefs`;
803    /// non-HTTPS listeners escape through this arm).
804    FieldNotEquals {
805        /// Relative field path compared with the literal.
806        path: Vec<String>,
807        /// Literal excluded at the field path.
808        value: GuardValue,
809    },
810    /// The value must be of this JSON Schema type IF present and non-null:
811    /// Go's `eq`/`ne` compare `nil` against anything, so a missing or null
812    /// comparison operand renders while a present value of a different
813    /// basic kind aborts.
814    ComparableKind(String),
815    /// The value must NOT be of this JSON Schema type.
816    NotSchemaType(String),
817    /// The value must be an object containing this member.
818    HasMember(String),
819    /// The value must be an object containing this member EVEN when the
820    /// chart's own defaults supply it: the consumer aborts on an absent
821    /// subject (a nil `dig` dict), and under coalesced-document semantics
822    /// the member is absent exactly when a user null-deletes it — the
823    /// state the requirement must reject. Exempt from the
824    /// default-supplied `required` relaxation that render-grade presence
825    /// claims get.
826    HasMemberEvenDefaulted(String),
827    /// The value must be a string matching this regular expression
828    /// (`regexMatch` type-asserts a string subject, so string-ness rides
829    /// along).
830    MatchesPattern {
831        /// Regular expression the string must match.
832        pattern: String,
833        /// Whether the pattern originated from a templated expression.
834        templated: bool,
835    },
836    /// The value must be a string NOT matching this regular expression —
837    /// the failing test fired on matches, and its `regexMatch` still
838    /// type-asserts a string subject (traefik's uppercase key gate).
839    NotMatchesPattern {
840        /// Regular expression the string must not match.
841        pattern: String,
842    },
843    /// The value must be a string whose length lies inside the window — a
844    /// provider key slot's `minLength`/`maxLength` projected onto a ranged
845    /// collection's keys (traefik's Gateway listener names).
846    StringLengthBounds {
847        /// Inclusive minimum length, when one is known.
848        min: Option<u64>,
849        /// Inclusive maximum length, when one is known.
850        max: Option<u64>,
851    },
852    /// The value HOSTS literal member reads: it must be an object — or one
853    /// of the kinds the chart's own type dispatch provably handles before
854    /// the reads run (nack converts the string image form with `set`).
855    MemberHost {
856        /// Non-object JSON kinds explicitly handled by chart dispatch.
857        handled_kinds: Vec<String>,
858        /// Whether this arm belongs to the exact, complete access domain.
859        /// A sound-subset arm may reject states where navigation certainly
860        /// executes, but it must not decide ownership of the path's base.
861        complete_domain: bool,
862    },
863    /// The value is iterated by `range`: collections and nil render, and
864    /// integer counts iterate when the loop body has no member structure.
865    Iterable {
866        /// Whether Helm's integer-count range form remains accepted.
867        allow_integer: bool,
868    },
869    /// A zero-based position must exist before `index` can project it.
870    /// Arrays lower exactly; strings remain conservative because Go indexes
871    /// bytes while JSON Schema `minLength` counts Unicode code points.
872    IndexableAt(usize),
873    /// Splitting the textual form must produce at least `segments` entries.
874    /// When the input was first passed through a total text conversion,
875    /// non-string inputs remain conservatively accepted.
876    SplitSegmentsAtLeast {
877        /// Literal delimiter used by the split operation.
878        separator: String,
879        /// Minimum number of produced segments.
880        segments: usize,
881        /// Whether a preceding total conversion admits non-string inputs.
882        allow_non_string: bool,
883    },
884    /// The value renders inside a manually quoted YAML scalar: every string
885    /// it contributes to the token — the value itself, or any nested string
886    /// or mapping key when Go's fmt serializes a collection
887    /// (`map[k:v]` / `[a b]`) with its strings embedded raw — must be valid
888    /// content for the quoting style. Non-string scalars format as plain
889    /// digits/words and are always safe.
890    QuotedSerializationSafe {
891        /// YAML quoting grammar that serialized content must satisfy.
892        style: QuotedScalarStyle,
893        /// The rendered text is a `tpl` result, so template-action-free
894        /// input is the identity while an actual program has unknown output.
895        templated: bool,
896    },
897    /// A value substituted by fmt's `%s` may be a string or a mapping whose
898    /// recursive textual form remains structurally safe. Other JSON kinds
899    /// emit a leading fmt diagnostic or collection indicator.
900    PrintfStringOperand,
901    /// The value's own text renders into an UNQUOTED YAML slot, so text that
902    /// closes the plain token there corrupts the document: a `: ` (or
903    /// trailing `:`) turns the slot into a nested mapping, a ` #` truncates
904    /// it as a comment, and a line break ends it. Non-string scalars format
905    /// as plain digits/words and are always safe.
906    ///
907    /// This is a STRUCTURAL claim only — the resolver-token exclusions that
908    /// keep a plain token from reparsing as a number/bool/null belong to the
909    /// provider-slot preimage, which knows the sink's declared type.
910    PlainScalarSafe {
911        /// The text OPENS the token, so leading YAML indicators break it too.
912        /// False for a splice with literal text ahead of it.
913        token_initial: bool,
914        /// The rendered text is a `tpl` render of the value, which is the
915        /// identity only on template-ACTION-free input: a value carrying
916        /// `{{` renders to something else entirely and escapes the claim.
917        templated: bool,
918    },
919}
920
921impl ContractPathSchemaEvidence {
922    /// Reports whether positive, unconditional evidence can make the path required.
923    #[must_use]
924    pub fn is_required_inference_candidate(&self) -> bool {
925        self.requiredness.is_positive_header
926            && !self.requiredness.has_default_fallback
927            && !self.requiredness.is_conditionally_optional
928            && self.facts.has_non_self_guarded_render_use()
929    }
930}
931
932/// Contract-derived facts consumed by core values-schema generation.
933///
934/// This is the typed boundary between static template interpretation and JSON
935/// Schema lowering. Optional post-passes can ask for their own projections,
936/// but core schema generation should consume this artifact rather than
937/// re-reading raw contract claims.
938#[derive(Debug, Clone, Default, PartialEq, Eq)]
939pub struct ContractSchemaSignals {
940    schema_evidence_by_value_path: BTreeMap<crate::ValuesPath, ContractPathSchemaEvidence>,
941    referenced_value_paths: BTreeSet<crate::ValuesPath>,
942    pruned_parent_value_paths: BTreeSet<crate::ValuesPath>,
943    unconditionally_omitted_value_paths: BTreeSet<crate::ValuesPath>,
944    direct_ranged_value_paths: BTreeSet<crate::ValuesPath>,
945    values_default_sources: BTreeSet<ValuesDefaultSource>,
946    guarded_values_default_sources: BTreeSet<GuardedValuesDefaultSource>,
947    values_program_wrappers: BTreeSet<ValuesProgramWrapper>,
948    /// Values paths whose nodes must not gain a wrapper alternative: a
949    /// strict string consumer reads them before the engine's values-root
950    /// rewrite, so a wrapper map there aborts rendering.
951    values_program_wrapper_exclusions: BTreeSet<crate::ValuesPath>,
952    /// Terminating validator formulas: rendering aborts whenever ALL guards
953    /// of one clause hold, so no valid values document may satisfy them
954    /// (`fail`/`required` under fully lowerable conditions). An empty clause
955    /// is an unconditional termination.
956    terminal_clauses: Vec<Vec<ConditionalGuard>>,
957}
958
959impl ContractSchemaSignals {
960    /// Builds a stable signal set from path evidence and terminal clauses.
961    #[must_use]
962    pub fn new(
963        schema_evidence_by_value_path: BTreeMap<crate::ValuesPath, ContractPathSchemaEvidence>,
964        terminal_clauses: Vec<Vec<ConditionalGuard>>,
965    ) -> Self {
966        let referenced_value_paths: BTreeSet<crate::ValuesPath> = schema_evidence_by_value_path
967            .iter()
968            .filter(|(_, evidence)| evidence.is_referenced_value_path)
969            .map(|(path, _)| path.clone())
970            .collect();
971        let pruned_parent_value_paths = schema_evidence_by_value_path
972            .iter()
973            .filter(|(_, evidence)| {
974                evidence.facts.has_referenced_descendants && !evidence.facts.used_as_fragment
975            })
976            .map(|(path, _)| path.clone())
977            .collect();
978        let unconditionally_omitted_value_paths = schema_evidence_by_value_path
979            .iter()
980            .flat_map(|(path, evidence)| {
981                let mut provider_uses = evidence.provider_schema_uses.iter().chain(
982                    evidence
983                        .conditional_overlays
984                        .iter()
985                        .flat_map(|overlay| overlay.evidence.provider_schema_uses.iter()),
986                );
987                let Some(first_use) = provider_uses.next() else {
988                    return BTreeSet::new();
989                };
990                let mut omitted_members = first_use
991                    .omitted_members
992                    .iter()
993                    .filter(|(_, retain_guards)| retain_guards.is_empty())
994                    .map(|(member, _)| member.clone())
995                    .collect::<BTreeSet<_>>();
996                for provider_use in provider_uses {
997                    omitted_members.retain(|member| {
998                        provider_use
999                            .omitted_members
1000                            .get(member)
1001                            .is_some_and(Vec::is_empty)
1002                    });
1003                }
1004                omitted_members
1005                    .into_iter()
1006                    .map(|member| {
1007                        let mut member_path = path.clone();
1008                        member_path.push(member);
1009                        member_path
1010                    })
1011                    .filter(|member_path| referenced_value_paths.contains(member_path))
1012                    .collect()
1013            })
1014            .collect();
1015        let direct_ranged_value_paths = schema_evidence_by_value_path
1016            .iter()
1017            .filter(|(_, evidence)| evidence.facts.is_direct_ranged_source)
1018            .map(|(path, _)| path.clone())
1019            .collect();
1020        Self {
1021            schema_evidence_by_value_path,
1022            referenced_value_paths,
1023            pruned_parent_value_paths,
1024            unconditionally_omitted_value_paths,
1025            direct_ranged_value_paths,
1026            values_default_sources: BTreeSet::new(),
1027            guarded_values_default_sources: BTreeSet::new(),
1028            values_program_wrappers: BTreeSet::new(),
1029            values_program_wrapper_exclusions: BTreeSet::new(),
1030            terminal_clauses,
1031        }
1032    }
1033
1034    /// Attaches chart subtrees that supply runtime defaults to effective values paths.
1035    #[must_use]
1036    pub fn with_values_default_sources(
1037        mut self,
1038        sources: impl IntoIterator<Item = ValuesDefaultSource>,
1039    ) -> Self {
1040        self.values_default_sources.extend(sources);
1041        self
1042    }
1043
1044    /// Default subtrees applied to effective values before templates consume them.
1045    #[must_use]
1046    pub fn values_default_sources(&self) -> &BTreeSet<ValuesDefaultSource> {
1047        &self.values_default_sources
1048    }
1049
1050    /// Attaches runtime default sources scoped by chart activation.
1051    #[must_use]
1052    pub fn with_guarded_values_default_sources(
1053        mut self,
1054        sources: impl IntoIterator<Item = GuardedValuesDefaultSource>,
1055    ) -> Self {
1056        self.guarded_values_default_sources.extend(sources);
1057        self
1058    }
1059
1060    /// Runtime default sources that must not enter unconditional composition.
1061    #[must_use]
1062    pub fn guarded_values_default_sources(&self) -> &BTreeSet<GuardedValuesDefaultSource> {
1063        &self.guarded_values_default_sources
1064    }
1065
1066    /// Projects fail-grade contracts on effective-root paths onto their
1067    /// prefixed spellings for every in-place root overlay
1068    /// (`mustMergeOverwrite $.Values (index $.Values "pilot")`): a member
1069    /// the user writes under the prefix overwrites its effective-root twin
1070    /// before any consumer reads it, so the same abort-grade requirements
1071    /// bind the prefixed path (istiod's `pilot.env: "oops"` aborts exactly
1072    /// like `env: "oops"`). Guards about the subject path or its
1073    /// descendants move to the prefixed spelling; foreign guard paths keep
1074    /// their root spellings — a bounded reading that assumes cross-path
1075    /// conditions are supplied at the root, not through the same overlay.
1076    #[must_use]
1077    pub fn with_root_overlay_requirement_implications(
1078        self,
1079        prefixes: impl IntoIterator<Item = crate::ValuesPath>,
1080    ) -> Self {
1081        self.with_guarded_root_overlay_requirement_implications(
1082            prefixes
1083                .into_iter()
1084                .map(|prefix| (Vec::<ConditionalGuard>::new(), prefix)),
1085        )
1086    }
1087
1088    /// Projects abort-grade root-overlay requirements under activation guards.
1089    #[must_use]
1090    pub fn with_guarded_root_overlay_requirement_implications(
1091        self,
1092        overlays: impl IntoIterator<Item = (Vec<ConditionalGuard>, crate::ValuesPath)>,
1093    ) -> Self {
1094        self.with_scoped_guarded_root_overlay_requirement_implications(
1095            overlays
1096                .into_iter()
1097                .map(|(guards, source_path)| (guards, crate::ValuesPath::default(), source_path)),
1098        )
1099    }
1100
1101    /// Projects abort-grade requirements across a scoped in-place values overlay.
1102    #[must_use]
1103    pub fn with_scoped_guarded_root_overlay_requirement_implications(
1104        mut self,
1105        overlays: impl IntoIterator<
1106            Item = (Vec<ConditionalGuard>, crate::ValuesPath, crate::ValuesPath),
1107        >,
1108    ) -> Self {
1109        for (activation_guards, target_path, source_path) in overlays {
1110            self.project_root_overlay_requirements(&target_path, &source_path, &activation_guards);
1111        }
1112        self
1113    }
1114
1115    fn project_root_overlay_requirements(
1116        &mut self,
1117        target_path: &crate::ValuesPath,
1118        source_path: &crate::ValuesPath,
1119        activation_guards: &[ConditionalGuard],
1120    ) {
1121        if source_path.segments().next().is_none() {
1122            return;
1123        }
1124        let twins: Vec<(crate::ValuesPath, Vec<ContractRequirementImplication>)> = self
1125            .schema_evidence_by_value_path
1126            .iter()
1127            .filter(|(path, evidence)| {
1128                !evidence.requirement_implications.is_empty()
1129                    && (*path == target_path || path.is_descendant_of(target_path))
1130                    && *path != source_path
1131                    && !path.is_descendant_of(source_path)
1132                    && !source_path.is_descendant_of(path)
1133            })
1134            .map(|(path, evidence)| {
1135                let implications = evidence
1136                    .requirement_implications
1137                    .iter()
1138                    .map(|implication| {
1139                        let mut twin = implication.clone();
1140                        twin.outer_guards = twin
1141                            .outer_guards
1142                            .into_iter()
1143                            .map(|guard| {
1144                                guard.map_value_paths(&mut |guard_path| {
1145                                    if &guard_path == path || guard_path.is_descendant_of(path) {
1146                                        overlay_source_value_path(
1147                                            target_path,
1148                                            source_path,
1149                                            &guard_path,
1150                                        )
1151                                    } else {
1152                                        guard_path
1153                                    }
1154                                })
1155                            })
1156                            .chain(activation_guards.iter().cloned())
1157                            .collect();
1158                        twin.outer_guards.sort();
1159                        twin.outer_guards.dedup();
1160                        twin
1161                    })
1162                    .collect();
1163                (
1164                    overlay_source_value_path(target_path, source_path, path),
1165                    implications,
1166                )
1167            })
1168            .collect();
1169        for (twin_path, implications) in twins {
1170            let entry = self
1171                .schema_evidence_by_value_path
1172                .entry(twin_path)
1173                .or_default();
1174            for implication in implications {
1175                if !entry.requirement_implications.contains(&implication) {
1176                    entry.requirement_implications.push(implication);
1177                }
1178            }
1179        }
1180    }
1181
1182    /// Attaches chart-authored program-wrapper conventions.
1183    #[must_use]
1184    pub fn with_values_program_wrappers(
1185        mut self,
1186        wrappers: impl IntoIterator<Item = ValuesProgramWrapper>,
1187    ) -> Self {
1188        self.values_program_wrappers.extend(wrappers);
1189        self
1190    }
1191
1192    /// Program-wrapper conventions the chart's engine applies to its values.
1193    #[must_use]
1194    pub fn values_program_wrappers(&self) -> &BTreeSet<ValuesProgramWrapper> {
1195        &self.values_program_wrappers
1196    }
1197
1198    /// Attaches paths excluded from wrapper alternatives (pre-rewrite
1199    /// strict consumers).
1200    #[must_use]
1201    pub fn with_values_program_wrapper_exclusions(
1202        mut self,
1203        paths: impl IntoIterator<Item = crate::ValuesPath>,
1204    ) -> Self {
1205        self.values_program_wrapper_exclusions.extend(paths);
1206        self
1207    }
1208
1209    /// Values paths whose nodes must not gain a wrapper alternative.
1210    #[must_use]
1211    pub fn values_program_wrapper_exclusions(&self) -> &BTreeSet<crate::ValuesPath> {
1212        &self.values_program_wrapper_exclusions
1213    }
1214
1215    /// Paths the chart ranges DIRECTLY: their runtime iterable domain is
1216    /// wider than any declared shape, so ancestor subtree schemas must not
1217    /// shadow their own resolutions.
1218    #[must_use]
1219    pub fn direct_ranged_value_paths(&self) -> &BTreeSet<crate::ValuesPath> {
1220        &self.direct_ranged_value_paths
1221    }
1222
1223    /// Terminating validator formulas: no valid values document satisfies
1224    /// all guards of one clause.
1225    #[must_use]
1226    pub fn terminal_clauses(&self) -> &[Vec<ConditionalGuard>] {
1227        &self.terminal_clauses
1228    }
1229
1230    /// Returns schema-lowering evidence indexed by canonical values path.
1231    #[must_use]
1232    pub fn schema_evidence_by_value_path(
1233        &self,
1234    ) -> &BTreeMap<crate::ValuesPath, ContractPathSchemaEvidence> {
1235        &self.schema_evidence_by_value_path
1236    }
1237
1238    /// Values paths the contract directly referenced, in stable order.
1239    #[must_use]
1240    pub fn referenced_value_paths(&self) -> &BTreeSet<crate::ValuesPath> {
1241        &self.referenced_value_paths
1242    }
1243
1244    /// Non-fragment parent paths whose referenced descendants own their own
1245    /// schema evidence, so parent-level defaults must not restate them.
1246    #[must_use]
1247    pub fn pruned_parent_value_paths(&self) -> &BTreeSet<crate::ValuesPath> {
1248        &self.pruned_parent_value_paths
1249    }
1250
1251    /// Referenced members removed before every provider sink of their parent.
1252    #[must_use]
1253    pub fn unconditionally_omitted_value_paths(&self) -> &BTreeSet<crate::ValuesPath> {
1254        &self.unconditionally_omitted_value_paths
1255    }
1256
1257    /// Returns schema evidence for one canonical values path.
1258    #[must_use]
1259    pub fn evidence_for(
1260        &self,
1261        value_path: &crate::ValuesPath,
1262    ) -> Option<&ContractPathSchemaEvidence> {
1263        self.schema_evidence_by_value_path.get(value_path)
1264    }
1265}
1266
1267fn overlay_source_value_path(
1268    target_path: &crate::ValuesPath,
1269    source_path: &crate::ValuesPath,
1270    path: &crate::ValuesPath,
1271) -> crate::ValuesPath {
1272    let target_segments = target_path.segments().collect::<Vec<_>>();
1273    let path_segments = path.segments().collect::<Vec<_>>();
1274    let suffix = path_segments
1275        .strip_prefix(target_segments.as_slice())
1276        .unwrap_or(path_segments.as_slice());
1277    crate::ValuesPath::from_segments(source_path.segments().chain(suffix.iter().copied()))
1278}
1279
1280/// Universally quantified fact whose empty-set identity is `true`.
1281#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1282pub struct AllUses(bool);
1283
1284impl Default for AllUses {
1285    fn default() -> Self {
1286        Self(true)
1287    }
1288}
1289
1290impl AllUses {
1291    /// Creates a universal fact from an already-aggregated result.
1292    #[must_use]
1293    pub const fn new(value: bool) -> Self {
1294        Self(value)
1295    }
1296
1297    /// Returns the quantified Boolean result.
1298    #[must_use]
1299    pub const fn holds(self) -> bool {
1300        self.0
1301    }
1302}
1303
1304impl std::ops::BitAndAssign<bool> for AllUses {
1305    fn bitand_assign(&mut self, rhs: bool) {
1306        self.0 &= rhs;
1307    }
1308}
1309
1310impl std::ops::BitAndAssign<Self> for AllUses {
1311    fn bitand_assign(&mut self, rhs: Self) {
1312        self.0 &= rhs.0;
1313    }
1314}
1315
1316/// Schema-generation facts for one input values path.
1317///
1318/// This bundles the contract-owned path state that schema lowering needs, so
1319/// generator code does not have to reconstruct semantic facts from multiple
1320/// lower-level projections.
1321#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1322pub struct ContractValuePathFacts {
1323    /// Whether analysis observed referenced paths below this path.
1324    pub has_referenced_descendants: bool,
1325    /// Descendant rows that continue through a `*` item segment. Item rows
1326    /// describe a ranged collection's element shape; a literal member read
1327    /// (e.g. a guard probing one key of a user-populated map) does not.
1328    pub has_item_descendants: bool,
1329    /// Item descendants that continue INTO element structure (`p.*.field`).
1330    /// A bare `p.*` value row proves no LIST shape: `range` iterates maps
1331    /// too, so declared-empty maps with only bare member-value rows stay
1332    /// user-populated.
1333    pub has_structured_item_descendants: bool,
1334    /// Whether the path renders as a structural YAML fragment.
1335    pub used_as_fragment: bool,
1336    /// The path renders through a serializing or total-stringification sink
1337    /// (`tpl (toYaml …)`, `quote`, `toString`, `join`): any input type
1338    /// renders, so the use exposes provenance but no input shape.
1339    pub used_as_serialized: bool,
1340    /// The path is rendered through `toYaml`. The input kind is unrestricted,
1341    /// while the resulting YAML fragment still obeys structural placement.
1342    pub used_as_yaml_serialized: bool,
1343    /// A string-consuming transform (`trunc`, `b64enc`, `fromYaml`, a
1344    /// dynamic `printf` format) bound a real runtime string contract on the
1345    /// path: rendering fails for non-string values, so this typing survives
1346    /// even when another use stringifies the path.
1347    pub has_string_contract: bool,
1348    /// Whether a runtime string contract can consume the raw value without
1349    /// first passing through that value's own truthiness guard. Such a
1350    /// contract still rejects Helm-falsy non-strings even when every placed
1351    /// render row is self-guarded.
1352    pub has_non_self_guarded_string_contract: bool,
1353    /// Some `path.*` member row carries a runtime string contract (`tpl`
1354    /// over each ranged member): integer iteration yields int members the
1355    /// contract rejects, so the integer lane closes.
1356    pub has_string_contract_items: bool,
1357    /// Whether fragment rendering lost a precise output location.
1358    pub used_as_pathless_fragment: bool,
1359    /// Whether the path may supply the chart's complete values-root fragment.
1360    pub accepted_values_root_fragment: bool,
1361    /// Whether the path may supply a dependency values-root fragment.
1362    pub accepted_dependency_values_root_fragment: bool,
1363    /// Whether this path or one of its projections supplies a range action.
1364    pub is_ranged_source: bool,
1365    /// The chart ranges this path DIRECTLY (`range .Values.x`), so the
1366    /// runtime iterable domain applies to the path's own value.
1367    pub is_direct_ranged_source: bool,
1368    /// Some direct range over this path uses TWO variables
1369    /// (`range $k, $v := …`): integers iterate single-variable ranges only
1370    /// ("can't use 2 to iterate over more than one variable").
1371    pub has_destructured_range_use: bool,
1372    /// Some direct range sees the path after JSON decoding, where numbers are
1373    /// `float64` values rather than Helm's integer iteration counts.
1374    pub has_json_decoded_range_use: bool,
1375    /// Whether the path contributes only part of a rendered scalar token.
1376    pub is_partial_scalar_value_path: bool,
1377    /// Whether any rendering sink consumes the path.
1378    pub has_render_use: bool,
1379    /// Whether any use consumes the value rather than merely testing it in a
1380    /// positive control-flow header.
1381    pub has_non_control_use: bool,
1382    /// Whether a non-control consumer observes the value outside an ordered
1383    /// merge layer. Such a consumer keeps the resolved base beside synthesized
1384    /// merge-layer arms.
1385    pub has_unlayered_non_control_use: bool,
1386    /// Whether a rendering sink consumes the path without a branch guard.
1387    pub has_unconditional_render_use: bool,
1388    /// Whether any rendering sink is guarded by this path's own truthiness.
1389    pub has_self_guarded_render_use: bool,
1390    /// Whether every rendering sink is guarded by this path's own truthiness.
1391    pub all_render_uses_self_guarded: AllUses,
1392    /// A render consumed this path as one layer of an ordered merge: the
1393    /// generator synthesizes the layer's typing as root arms, and the
1394    /// layer's synthetic self-truthiness guard must not drive base
1395    /// classification (a declared `{}` default stays an open map — the
1396    /// merged sink renders any user-supplied members).
1397    pub has_merge_layered_use: bool,
1398    /// A merge layer passes through Helm's map-only YAML decoder. Provider
1399    /// typing applies to mapping inputs, while non-mapping source shapes are
1400    /// discarded and therefore must remain open in the base schema.
1401    pub has_parsed_map_layered_use: bool,
1402    /// Every render use either sits behind the path's own truthy selection or
1403    /// cannot reject a Helm-falsy value at all: a `merge` operand's strict
1404    /// map contract rides its requirement implication (which keys on the call's live
1405    /// gate), and a checksum digest row hashes re-rendered text without
1406    /// consuming the raw value. Unlike `all_render_uses_self_guarded`, this
1407    /// bit feeds ONLY the base falsy escape — never overlay-branch routing or
1408    /// declared-default placement.
1409    pub all_render_uses_falsy_tolerant: AllUses,
1410    /// Whether a direct range guard protects a rendering sink for this path.
1411    pub has_self_range_guard_render_use: bool,
1412    /// Whether observed semantics explicitly admit null.
1413    pub is_nullable: bool,
1414}
1415
1416#[expect(
1417    clippy::derivable_impls,
1418    reason = "the exhaustive field list makes every new semantic fact choose its aggregation identity"
1419)]
1420impl Default for ContractValuePathFacts {
1421    fn default() -> Self {
1422        Self {
1423            has_referenced_descendants: false,
1424            has_item_descendants: false,
1425            has_structured_item_descendants: false,
1426            used_as_fragment: false,
1427            used_as_serialized: false,
1428            used_as_yaml_serialized: false,
1429            has_string_contract: false,
1430            has_non_self_guarded_string_contract: false,
1431            has_string_contract_items: false,
1432            used_as_pathless_fragment: false,
1433            accepted_values_root_fragment: false,
1434            accepted_dependency_values_root_fragment: false,
1435            is_ranged_source: false,
1436            is_direct_ranged_source: false,
1437            has_destructured_range_use: false,
1438            has_json_decoded_range_use: false,
1439            is_partial_scalar_value_path: false,
1440            has_render_use: false,
1441            has_non_control_use: false,
1442            has_unlayered_non_control_use: false,
1443            has_unconditional_render_use: false,
1444            has_self_guarded_render_use: false,
1445            all_render_uses_self_guarded: AllUses::default(),
1446            has_merge_layered_use: false,
1447            has_parsed_map_layered_use: false,
1448            all_render_uses_falsy_tolerant: AllUses::default(),
1449            has_self_range_guard_render_use: false,
1450            is_nullable: false,
1451        }
1452    }
1453}
1454
1455impl ContractValuePathFacts {
1456    /// Incorporates one rendering use into the aggregate path facts.
1457    pub fn record_render_use(
1458        &mut self,
1459        range_guarded: bool,
1460        self_guarded: Option<bool>,
1461        falsy_tolerant: Option<bool>,
1462    ) {
1463        if !self.has_render_use {
1464            self.all_render_uses_self_guarded = AllUses::default();
1465            self.all_render_uses_falsy_tolerant = AllUses::default();
1466        }
1467        self.has_render_use = true;
1468        self.has_self_range_guard_render_use |= range_guarded;
1469        if let Some(self_guarded) = self_guarded {
1470            self.has_self_guarded_render_use |= self_guarded;
1471            self.all_render_uses_self_guarded &= self_guarded;
1472        }
1473        if let Some(falsy_tolerant) = falsy_tolerant {
1474            self.all_render_uses_falsy_tolerant &= falsy_tolerant;
1475        }
1476    }
1477
1478    /// Merges rendering facts collected by another analysis branch.
1479    pub fn merge_render_use_facts(&mut self, other: Self) {
1480        let Self {
1481            has_referenced_descendants: _,
1482            has_item_descendants: _,
1483            has_structured_item_descendants: _,
1484            used_as_fragment: _,
1485            used_as_serialized: _,
1486            used_as_yaml_serialized: _,
1487            has_string_contract: _,
1488            has_non_self_guarded_string_contract: _,
1489            has_string_contract_items: _,
1490            used_as_pathless_fragment: _,
1491            accepted_values_root_fragment: _,
1492            accepted_dependency_values_root_fragment: _,
1493            is_ranged_source: _,
1494            is_direct_ranged_source: _,
1495            has_destructured_range_use: _,
1496            has_json_decoded_range_use: _,
1497            is_partial_scalar_value_path: _,
1498            has_render_use,
1499            has_non_control_use: _,
1500            has_unlayered_non_control_use: _,
1501            has_unconditional_render_use,
1502            has_self_guarded_render_use,
1503            all_render_uses_self_guarded,
1504            has_merge_layered_use,
1505            has_parsed_map_layered_use,
1506            all_render_uses_falsy_tolerant,
1507            has_self_range_guard_render_use,
1508            is_nullable: _,
1509        } = other;
1510        if !has_render_use {
1511            return;
1512        }
1513        if !self.has_render_use {
1514            self.all_render_uses_self_guarded = AllUses::default();
1515            self.all_render_uses_falsy_tolerant = AllUses::default();
1516        }
1517        self.has_render_use = true;
1518        self.has_unconditional_render_use |= has_unconditional_render_use;
1519        self.has_self_guarded_render_use |= has_self_guarded_render_use;
1520        self.has_merge_layered_use |= has_merge_layered_use;
1521        self.has_parsed_map_layered_use |= has_parsed_map_layered_use;
1522        self.has_self_range_guard_render_use |= has_self_range_guard_render_use;
1523        self.all_render_uses_self_guarded &= all_render_uses_self_guarded;
1524        self.all_render_uses_falsy_tolerant &= all_render_uses_falsy_tolerant;
1525    }
1526
1527    #[must_use]
1528    pub(crate) fn has_non_self_guarded_render_use(self) -> bool {
1529        self.has_render_use
1530            && !self.has_self_guarded_render_use
1531            && !self.all_render_uses_self_guarded.holds()
1532    }
1533}
1534
1535/// Path-local evidence consumed by the optional `--infer-required` post-pass.
1536///
1537/// These are still static-analysis facts, not a decision that the path must be
1538/// required. The generator combines them with render-use facts and chart
1539/// defaults before mutating the JSON Schema.
1540#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1541pub struct ContractRequirednessEvidence {
1542    /// Whether the path appears in a positive control-flow header.
1543    pub is_positive_header: bool,
1544    /// Whether some branch permits the path to remain absent.
1545    pub is_conditionally_optional: bool,
1546    /// Whether a defaulting operation supplies an absent value.
1547    pub has_default_fallback: bool,
1548}