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}