use std::collections::{BTreeMap, BTreeSet};
use helm_schema_core::{
ConditionalGuard, ConditionalPathOverlay, ContractSchemaSignals, GuardValue,
ProviderSchemaFragment, ResourceSchemaOracle, ValuesPath,
};
use serde_json::Value;
use serde_yaml::Value as YamlValue;
use crate::common_prefix_len;
use crate::condition_encoding::{
build_condition_clauses, evaluate_guard_set_on_values, guard_encodes_fully,
};
use crate::emission_policy::{
ConditionalFlavor, EmissionClass, EmissionOrigin, GuardScopes, NestedGuardScope, TerminalWhen,
};
use crate::emission_report::{EmissionReport, InsertionAbstentionCounts};
use crate::path_resolver::{PathSchemaResolver, ResolvedPathSchema};
use crate::provider_schema::ProviderSchemaCandidate;
use crate::resolve_policy::conditional_target_schema;
use crate::schema_node::SchemaNode;
use crate::schema_tree::SchemaDocument;
use crate::values_yaml::yaml_value_at_values_path;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ConditionalBaseEffect {
None,
Own,
Preserve,
Require,
}
#[derive(Debug, Clone)]
pub(crate) struct ConjunctCarrier {
pub(crate) target_value_path: ValuesPath,
pub(crate) ancestor_segments: Vec<String>,
pub(crate) relative_target_segments: Vec<String>,
pub(crate) base_effect: ConditionalBaseEffect,
relax_untyped_host: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct LoweredConjunct {
pub(crate) class: EmissionClass,
pub(crate) origin: EmissionOrigin,
pub(crate) carrier: ConjunctCarrier,
pub(crate) schema: Value,
pub(crate) provider_candidate: Option<ProviderSchemaCandidate>,
}
impl LoweredConjunct {
#[expect(
clippy::too_many_arguments,
reason = "the constructor makes the complete lowered carrier and its policy class auditable at every producer"
)]
fn schema(
origin: EmissionOrigin,
flavor: ConditionalFlavor,
target_value_path: ValuesPath,
ancestor_segments: Vec<String>,
relative_target_segments: Vec<String>,
guards: Vec<ConditionalGuard>,
nested_guard_scopes: Vec<NestedGuardScope>,
target_schema: Value,
provider_schema_candidate: Option<ProviderSchemaCandidate>,
base_effect: ConditionalBaseEffect,
relax_untyped_host: bool,
) -> Self {
let class = EmissionClass::conditional(
GuardScopes::new(guards, nested_guard_scopes),
&ancestor_segments,
flavor,
);
Self {
class,
origin,
carrier: ConjunctCarrier {
target_value_path,
ancestor_segments,
relative_target_segments,
base_effect,
relax_untyped_host,
},
schema: target_schema,
provider_candidate: provider_schema_candidate,
}
}
pub(crate) fn terminal(guards: Vec<ConditionalGuard>) -> Self {
let class = if guards.is_empty() {
EmissionClass::terminal_always()
} else {
EmissionClass::terminal_guarded(guards).unwrap_or_else(EmissionClass::terminal_always)
};
Self {
class,
origin: EmissionOrigin::RequirementImplication,
carrier: ConjunctCarrier {
target_value_path: ValuesPath::default(),
ancestor_segments: Vec::new(),
relative_target_segments: Vec::new(),
base_effect: ConditionalBaseEffect::None,
relax_untyped_host: false,
},
schema: Value::Bool(false),
provider_candidate: None,
}
}
fn guard_scopes(&self) -> Option<&GuardScopes> {
match &self.class {
EmissionClass::Conditional { guards, .. } => Some(guards),
EmissionClass::Mandatory => Some(&EMPTY_GUARD_SCOPES),
EmissionClass::Terminal { .. } => None,
}
}
fn outer_guards(&self) -> &[ConditionalGuard] {
self.guard_scopes()
.map(|scopes| scopes.outer.as_slice())
.unwrap_or_default()
}
fn nested_guard_scopes(&self) -> &[NestedGuardScope] {
self.guard_scopes()
.map(|scopes| scopes.nested.as_slice())
.unwrap_or_default()
}
pub(crate) fn terminal_guards(&self) -> Option<&[ConditionalGuard]> {
match &self.class {
EmissionClass::Terminal {
when: TerminalWhen::Always,
} => Some(&[]),
EmissionClass::Terminal {
when: TerminalWhen::Guarded(scopes),
} => Some(scopes.scopes().outer.as_slice()),
EmissionClass::Mandatory | EmissionClass::Conditional { .. } => None,
}
}
}
static EMPTY_GUARD_SCOPES: GuardScopes = GuardScopes {
outer: Vec::new(),
nested: Vec::new(),
};
#[tracing::instrument(skip_all)]
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic lowering operation together makes its state transitions easier to audit"
)]
pub(crate) fn collect_conditional_schemas(
resolved_paths: &[ResolvedPathSchema],
contract_schema_signals: &ContractSchemaSignals,
values_yaml_doc: &YamlValue,
subchart_defaults_doc: &YamlValue,
provider: &dyn ResourceSchemaOracle,
) -> (Vec<LoweredConjunct>, InsertionAbstentionCounts) {
let mut insertion_abstentions = InsertionAbstentionCounts::default();
let mut synthesized_implications =
crate::provider_requirement_synthesis::synthesized_required_source_implications(
contract_schema_signals,
values_yaml_doc,
subchart_defaults_doc,
provider,
);
for (path, split_implications) in
crate::provider_requirement_synthesis::synthesized_split_segment_implications(
contract_schema_signals,
provider,
)
.into_iter()
.chain(
crate::provider_requirement_synthesis::synthesized_range_key_implications(
contract_schema_signals,
provider,
),
)
.chain(
crate::provider_requirement_synthesis::synthesized_ranged_member_required_implications(
contract_schema_signals,
subchart_defaults_doc,
provider,
),
)
{
let entries = synthesized_implications.entry(path).or_default();
for implication in split_implications {
if !entries.contains(&implication) {
entries.push(implication);
}
}
}
let resolved_by_path = resolved_paths
.iter()
.map(|resolved| (&resolved.value_path, resolved))
.collect::<BTreeMap<_, _>>();
let mut member_descendants: BTreeMap<&[String], Vec<&ResolvedPathSchema>> = BTreeMap::new();
for resolved in resolved_paths {
if let Some(star) = resolved
.path_segments
.iter()
.position(|segment| segment == "*")
{
member_descendants
.entry(resolved.path_segments.get(..star).unwrap_or_default())
.or_default()
.push(resolved);
}
}
let mut conditionals = Vec::new();
let root_path = ValuesPath::default();
if let Some(root_implications) = synthesized_implications.get(&root_path) {
for implication in root_implications {
if !implication.outer_guards.is_empty()
&& !implication_guards_supported(
&implication.outer_guards,
&root_path,
&resolved_by_path,
)
{
continue;
}
let (target_schema, abstentions) =
crate::path_resolver::fail_requirement_schema(std::iter::once(implication));
insertion_abstentions.requirement_target += abstentions;
if crate::schema_model::is_empty_schema(&target_schema) {
continue;
}
conditionals.push(LoweredConjunct::schema(
EmissionOrigin::Backprojection,
ConditionalFlavor::Ordinary,
root_path.clone(),
Vec::new(),
Vec::new(),
implication.outer_guards.clone(),
Vec::new(),
target_schema,
None,
ConditionalBaseEffect::None,
false,
));
}
}
for (target_value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
let Some(resolved_target) = resolved_by_path.get(target_value_path) else {
continue;
};
let has_unconditional_self_presence_contract = evidence
.conditional_overlays
.iter()
.any(|overlay| is_unconditional_self_presence_overlay(target_value_path, overlay));
let synthesized = synthesized_implications
.get(target_value_path)
.map(Vec::as_slice)
.unwrap_or_default();
let all_member_hosts_presence_scoped = {
let mut member_host_implications = evidence
.requirement_implications
.iter()
.chain(synthesized)
.filter(|implication| {
implication.requirements.iter().any(|requirement| {
matches!(
requirement,
helm_schema_core::FailValueRequirement::MemberHost { .. }
)
})
})
.peekable();
member_host_implications.peek().is_some()
&& member_host_implications.all(|implication| {
implication_has_self_presence_guard(implication, target_value_path)
})
};
for (implication, origin) in evidence
.requirement_implications
.iter()
.map(|implication| (implication, EmissionOrigin::RequirementImplication))
.chain(
synthesized
.iter()
.map(|implication| (implication, EmissionOrigin::Backprojection)),
)
{
if is_bare_iterable_implication(implication)
&& member_implication_covers_range_domain(
&evidence.requirement_implications,
&implication.outer_guards,
)
{
continue;
}
let member_host_only = !implication.requirements.is_empty()
&& implication.requirements.iter().all(|requirement| {
matches!(
requirement,
helm_schema_core::FailValueRequirement::MemberHost { .. }
)
});
let member_host_complete_domain = member_host_only
&& implication.requirements.iter().all(|requirement| {
matches!(
requirement,
helm_schema_core::FailValueRequirement::MemberHost {
complete_domain: true,
..
}
)
});
if !implication.outer_guards.is_empty()
&& !implication_guards_supported(
&implication.outer_guards,
target_value_path,
&resolved_by_path,
)
{
continue;
}
let (mut target_schema, abstentions) =
crate::path_resolver::fail_requirement_schema(std::iter::once(implication));
insertion_abstentions.requirement_target += abstentions;
if crate::schema_model::is_empty_schema(&target_schema) {
continue;
}
let abort_grade_presence = implication.requirements.iter().all(|requirement| {
matches!(
requirement,
helm_schema_core::FailValueRequirement::HasMemberEvenDefaulted(_)
)
});
if !abort_grade_presence
&& matches!(
&implication.target,
helm_schema_core::ContractRequirementTarget::Value
)
&& let Some(default) = yaml_value_at_values_path(values_yaml_doc, target_value_path)
{
relax_required_members_supplied_by_default(&mut target_schema, default);
}
let target_segments = target_value_path
.segments()
.map(helm_schema_core::Segment::encode_component)
.collect::<Vec<_>>();
if matches!(
&implication.target,
helm_schema_core::ContractRequirementTarget::Members { .. }
| helm_schema_core::ContractRequirementTarget::MembersExceptKeys { .. }
| helm_schema_core::ContractRequirementTarget::MembersWhereEquals { .. }
) && let Some(member_schema) = member_descendant_projection(
target_segments.as_slice(),
member_descendants
.get(target_segments.as_slice())
.map(Vec::as_slice)
.unwrap_or_default(),
&mut insertion_abstentions.conditional_member_projection,
) {
target_schema = crate::schema_tree::conjoin_collection_member_schema_value(
target_schema,
&member_schema,
);
}
let ancestor_segments: Vec<String> = Vec::new();
let presence_scoped_type_arm =
implication.requirements.iter().all(|requirement| {
matches!(
requirement,
helm_schema_core::FailValueRequirement::SchemaType(_)
| helm_schema_core::FailValueRequirement::SchemaTypeEvenNull(_)
)
}) && implication_has_self_presence_guard(implication, target_value_path);
let resolved_domain = &resolved_target.structural_schema;
let preserve_base_schema = (member_host_only && !member_host_complete_domain)
|| implication.outer_guards.is_empty()
|| (!implication_has_self_truthy_guard(implication, target_value_path)
&& !presence_scoped_type_arm
&& resolved_schema_admits_fail_requirement_domain(
resolved_domain,
implication,
));
let base_effect = if !preserve_base_schema {
ConditionalBaseEffect::Own
} else if member_host_only && !member_host_complete_domain {
ConditionalBaseEffect::Require
} else {
ConditionalBaseEffect::None
};
conditionals.push(LoweredConjunct::schema(
origin,
ConditionalFlavor::Ordinary,
target_value_path.clone(),
ancestor_segments.clone(),
target_segments
.get(ancestor_segments.len()..)
.unwrap_or_default()
.to_vec(),
implication.outer_guards.clone(),
Vec::new(),
target_schema,
None,
base_effect,
member_host_complete_domain && all_member_hosts_presence_scoped,
));
}
for source_overlay in &evidence.conditional_overlays {
let overlay = source_overlay;
let flavor = match overlay.flavor {
helm_schema_core::ConditionalOverlayFlavor::Ordinary => ConditionalFlavor::Ordinary,
helm_schema_core::ConditionalOverlayFlavor::KindBranch => {
ConditionalFlavor::KindPartition
}
};
if is_unconditional_self_presence_overlay(target_value_path, overlay) {
continue;
}
if !guards_supported_for_conditional_lowering(
&overlay.guards,
&resolved_by_path,
values_yaml_doc,
) {
continue;
}
let target_segments = target_value_path
.segments()
.map(helm_schema_core::Segment::encode_component)
.collect::<Vec<_>>();
let Some((outer_guards, nested_guard_scopes)) =
partition_guard_scopes(&target_segments, &overlay.guards)
else {
continue;
};
let ancestor_segments = nested_guard_scopes
.first()
.filter(|_| outer_guards.is_empty())
.map(|scope| {
let mut parent = scope.ancestor_segments.clone();
parent.pop();
parent
})
.unwrap_or_else(|| conditional_ancestor_segments(&target_segments, &outer_guards));
let active_by_defaults = evaluate_guard_set_on_values(&overlay.guards, values_yaml_doc);
let resolved_overlay =
resolve_overlay_target_schema(target_value_path, overlay, provider);
let preserve_overlay_base = !overlay.evidence.facts.is_ranged_source
&& (overlay.preserve_base_schema || has_unconditional_self_presence_contract);
let member_implication_owns_range_domain = overlay.evidence.facts.is_ranged_source
&& crate::schema_model::is_empty_schema(&resolved_overlay.schema)
&& member_implication_covers_range_domain(
&evidence.requirement_implications,
&overlay.guards,
);
if member_implication_owns_range_domain {
conditionals.push(LoweredConjunct::schema(
EmissionOrigin::Overlay,
flavor,
target_value_path.clone(),
ancestor_segments.clone(),
target_segments
.get(ancestor_segments.len()..)
.unwrap_or_default()
.to_vec(),
outer_guards.clone(),
nested_guard_scopes.clone(),
crate::schema_model::empty_schema(),
None,
if preserve_overlay_base {
ConditionalBaseEffect::Preserve
} else {
ConditionalBaseEffect::Own
},
false,
));
continue;
}
let range_allows_integer = overlay
.evidence
.range_domain
.is_some_and(helm_schema_core::RangeDomain::allows_integer);
let mut range_domain = crate::runtime_iterable_schema(range_allows_integer);
let mut member_schemas = Vec::new();
if let Some(member_schema) = member_descendant_projection(
target_segments.as_slice(),
member_descendants
.get(target_segments.as_slice())
.map(Vec::as_slice)
.unwrap_or_default(),
&mut insertion_abstentions.conditional_member_projection,
) {
member_schemas.push(member_schema);
}
if let Some(member_schema) =
structural_collection_member_projection(&resolved_target.structural_schema)
{
member_schemas.push(member_schema);
}
if !member_schemas.is_empty() {
let member_schema = crate::merge::merge_schema_list(member_schemas);
range_domain = crate::schema_tree::conjoin_collection_member_schema_value(
range_domain,
&member_schema,
);
}
let branch_schema = if overlay.evidence.facts.has_self_range_guard_render_use {
crate::merge::union_schema_list(vec![resolved_overlay.schema, range_domain])
} else if overlay.evidence.facts.is_ranged_source {
crate::merge::merge_schema_list(vec![resolved_overlay.schema, range_domain])
} else {
resolved_overlay.schema
};
let target_schema = conditional_target_schema(
target_value_path,
overlay,
values_yaml_doc,
branch_schema,
&resolved_target.values_yaml_schema,
resolved_target.schema.clone(),
active_by_defaults,
);
if crate::schema_model::is_empty_schema(&target_schema) {
if overlay.evidence.facts.used_as_serialized
|| overlay.evidence.facts.used_as_yaml_serialized
{
conditionals.push(LoweredConjunct::schema(
EmissionOrigin::Overlay,
flavor,
target_value_path.clone(),
ancestor_segments.clone(),
target_segments
.get(ancestor_segments.len()..)
.unwrap_or_default()
.to_vec(),
outer_guards.clone(),
nested_guard_scopes.clone(),
target_schema,
None,
if preserve_overlay_base {
ConditionalBaseEffect::Preserve
} else {
ConditionalBaseEffect::Own
},
false,
));
}
continue;
}
let provider_schema_candidate = resolved_overlay
.provider_schema_candidate
.filter(|candidate| candidate.survives_as(&target_schema));
conditionals.push(LoweredConjunct::schema(
EmissionOrigin::Overlay,
flavor,
target_value_path.clone(),
ancestor_segments.clone(),
target_segments
.get(ancestor_segments.len()..)
.unwrap_or_default()
.to_vec(),
outer_guards,
nested_guard_scopes,
target_schema,
provider_schema_candidate,
if preserve_overlay_base {
ConditionalBaseEffect::Preserve
} else {
ConditionalBaseEffect::Own
},
false,
));
}
}
append_merge_shadow_arms(&mut conditionals, contract_schema_signals, provider);
append_omitted_member_arms(&mut conditionals, contract_schema_signals, provider);
(conditionals, insertion_abstentions)
}
mod conditional_constraints;
mod member_projection;
pub(crate) use conditional_constraints::{
ConditionalHostPreparation, append_selected_constraints, append_terminal_clauses,
prepare_conditional_hosts,
};
use conditional_constraints::{
conditional_ancestor_segments, guards_supported_for_conditional_lowering,
implication_guards_supported, partition_guard_scopes, resolve_overlay_target_schema,
};
use member_projection::{
append_merge_shadow_arms, append_omitted_member_arms, implication_has_self_presence_guard,
implication_has_self_truthy_guard, is_bare_iterable_implication,
is_unconditional_self_presence_overlay, member_implication_covers_range_domain,
relax_required_members_supplied_by_default, resolved_schema_admits_fail_requirement_domain,
structural_collection_member_projection,
};
pub(crate) use member_projection::{member_descendant_projection, schema_runtime_types};