Skip to main content

helm_schema_gen/
emission_policy.rs

1//! Schema emission policy.
2
3use helm_schema_core::ConditionalGuard;
4use serde::Serialize;
5
6/// Version of the emission-policy vocabulary used in output annotations.
7pub const POLICY_VOCABULARY_VERSION: u64 = 1;
8
9/// Selects how much analyzed contract evidence is emitted as JSON Schema.
10///
11/// Profiles change only emission. They do not change chart analysis or the
12/// recovered contract. A reduced profile may remove constraints and therefore
13/// widen acceptance, but must never introduce a rejection that the full
14/// profile does not.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum SchemaProfile {
17    /// Emits every constraint supported by the schema backend.
18    #[default]
19    Full,
20    /// Omits document-level conditional validation while preserving base
21    /// path and provider constraints.
22    ///
23    /// This profile exists for Helm's validator, whose schema compilation
24    /// cost grows superlinearly on large conditional documents.
25    Lean,
26}
27
28impl SchemaProfile {
29    /// Stable profile spelling used in serialized policy metadata.
30    #[must_use]
31    pub const fn as_str(self) -> &'static str {
32        match self {
33            Self::Full => "full",
34            Self::Lean => "lean",
35        }
36    }
37
38    /// Resolves this preset into the complete version-1 policy vocabulary.
39    #[must_use]
40    pub const fn resolved_policy(self) -> ResolvedEmissionPolicy {
41        ResolvedEmissionPolicy {
42            requested_profile: Some(self),
43            policy: EmissionPolicy::for_profile(self),
44        }
45    }
46}
47
48/// A complete, valid selection over the version-1 emission vocabulary.
49///
50/// Construction is checked so callers cannot enable kind partitions while
51/// disabling every anchor capable of carrying them.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "kebab-case")]
54pub struct EmissionPolicy {
55    root_anchored_conditionals: bool,
56    local_conditionals: bool,
57    terminal_clauses: bool,
58    kind_partitions: bool,
59}
60
61/// Conditional anchor lanes selected by a complete emission policy.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ConditionalAnchors {
64    /// No conditional anchor lane is selected.
65    None,
66    /// Only document-root conditionals are selected.
67    Root,
68    /// Only locally anchored conditionals are selected.
69    Local,
70    /// Both root and local conditional anchors are selected.
71    RootAndLocal,
72}
73
74impl ConditionalAnchors {
75    /// Creates the exhaustive anchor selection from its two public knobs.
76    #[must_use]
77    pub const fn new(root_anchored_conditionals: bool, local_conditionals: bool) -> Self {
78        match (root_anchored_conditionals, local_conditionals) {
79            (false, false) => Self::None,
80            (true, false) => Self::Root,
81            (false, true) => Self::Local,
82            (true, true) => Self::RootAndLocal,
83        }
84    }
85
86    const fn root_selected(self) -> bool {
87        matches!(self, Self::Root | Self::RootAndLocal)
88    }
89
90    const fn local_selected(self) -> bool {
91        matches!(self, Self::Local | Self::RootAndLocal)
92    }
93}
94
95impl EmissionPolicy {
96    /// Creates an emission policy after checking the complete knob matrix.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error when kind partitions are enabled while both root and
101    /// local conditional anchors are disabled.
102    pub const fn new(
103        conditional_anchors: ConditionalAnchors,
104        terminal_clauses: bool,
105        kind_partitions: bool,
106    ) -> Result<Self, InvalidEmissionPolicy> {
107        let policy = Self {
108            root_anchored_conditionals: conditional_anchors.root_selected(),
109            local_conditionals: conditional_anchors.local_selected(),
110            terminal_clauses,
111            kind_partitions,
112        };
113        if policy.is_valid() {
114            Ok(policy)
115        } else {
116            Err(InvalidEmissionPolicy)
117        }
118    }
119
120    pub(crate) const fn for_profile(profile: SchemaProfile) -> Self {
121        match profile {
122            SchemaProfile::Full => Self {
123                root_anchored_conditionals: true,
124                local_conditionals: true,
125                terminal_clauses: true,
126                kind_partitions: true,
127            },
128            SchemaProfile::Lean => Self {
129                root_anchored_conditionals: false,
130                local_conditionals: true,
131                terminal_clauses: false,
132                kind_partitions: false,
133            },
134        }
135    }
136
137    pub(crate) const fn is_valid(self) -> bool {
138        !self.kind_partitions || self.root_anchored_conditionals || self.local_conditionals
139    }
140
141    const fn apply_delta(self, delta: EmissionPolicyDelta) -> Result<Self, InvalidEmissionPolicy> {
142        let root_anchored_conditionals = match delta.root_anchored_conditionals {
143            Some(value) => value,
144            None => self.root_anchored_conditionals,
145        };
146        let local_conditionals = match delta.local_conditionals {
147            Some(value) => value,
148            None => self.local_conditionals,
149        };
150        Self::new(
151            ConditionalAnchors::new(root_anchored_conditionals, local_conditionals),
152            match delta.terminal_clauses {
153                Some(value) => value,
154                None => self.terminal_clauses,
155            },
156            match delta.kind_partitions {
157                Some(value) => value,
158                None => self.kind_partitions,
159            },
160        )
161    }
162
163    /// Whether root-anchored ordinary conditionals are selected.
164    #[must_use]
165    pub const fn root_anchored_conditionals(self) -> bool {
166        self.root_anchored_conditionals
167    }
168
169    /// Whether locally anchored ordinary conditionals are selected.
170    #[must_use]
171    pub const fn local_conditionals(self) -> bool {
172        self.local_conditionals
173    }
174
175    /// Whether unconditional and guarded terminal clauses are selected.
176    #[must_use]
177    pub const fn terminal_clauses(self) -> bool {
178        self.terminal_clauses
179    }
180
181    /// Whether kind-partition refinements are selected at enabled anchors.
182    #[must_use]
183    pub const fn kind_partitions(self) -> bool {
184        self.kind_partitions
185    }
186
187    pub(crate) fn selects(self, class: &EmissionClass) -> bool {
188        match class {
189            EmissionClass::Mandatory => true,
190            EmissionClass::Conditional {
191                anchor,
192                flavor: ConditionalFlavor::Ordinary,
193                ..
194            } => {
195                if anchor.is_root() {
196                    self.root_anchored_conditionals
197                } else {
198                    self.local_conditionals
199                }
200            }
201            EmissionClass::Conditional {
202                anchor,
203                flavor: ConditionalFlavor::KindPartition,
204                ..
205            } => {
206                self.kind_partitions
207                    && if anchor.is_root() {
208                        self.root_anchored_conditionals
209                    } else {
210                        self.local_conditionals
211                    }
212            }
213            EmissionClass::Terminal { .. } => self.terminal_clauses,
214        }
215    }
216}
217
218/// Error returned for a contradictory emission knob matrix.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
220#[error("kind partitions require root-anchored-conditionals or local-conditionals to be enabled")]
221pub struct InvalidEmissionPolicy;
222
223/// Optional version-1 W-class knob changes applied over a profile preset.
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
225pub struct EmissionPolicyDelta {
226    root_anchored_conditionals: Option<bool>,
227    local_conditionals: Option<bool>,
228    terminal_clauses: Option<bool>,
229    kind_partitions: Option<bool>,
230}
231
232impl EmissionPolicyDelta {
233    /// Creates a delta covering every W-class knob.
234    #[must_use]
235    pub const fn new(
236        root_anchored_conditionals: Option<bool>,
237        local_conditionals: Option<bool>,
238        terminal_clauses: Option<bool>,
239        kind_partitions: Option<bool>,
240    ) -> Self {
241        Self {
242            root_anchored_conditionals,
243            local_conditionals,
244            terminal_clauses,
245            kind_partitions,
246        }
247    }
248
249    /// Optional root-anchored conditional override.
250    #[must_use]
251    pub const fn root_anchored_conditionals(self) -> Option<bool> {
252        self.root_anchored_conditionals
253    }
254
255    /// Optional local conditional override.
256    #[must_use]
257    pub const fn local_conditionals(self) -> Option<bool> {
258        self.local_conditionals
259    }
260
261    /// Optional terminal-clause override.
262    #[must_use]
263    pub const fn terminal_clauses(self) -> Option<bool> {
264        self.terminal_clauses
265    }
266
267    /// Optional kind-partition override.
268    #[must_use]
269    pub const fn kind_partitions(self) -> Option<bool> {
270        self.kind_partitions
271    }
272}
273
274/// Caller selection retaining either preset provenance or an explicit policy.
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum EmissionSelection {
277    /// A stable profile plus optional W-class knob changes.
278    Preset {
279        /// Requested stable profile.
280        profile: SchemaProfile,
281        /// W-class changes applied over the profile.
282        delta: EmissionPolicyDelta,
283    },
284    /// An explicitly constructed complete policy.
285    Explicit(EmissionPolicy),
286}
287
288impl EmissionSelection {
289    /// Resolves the selection once into its complete policy and provenance.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error when preset deltas produce a contradictory knob matrix.
294    pub const fn resolve(self) -> Result<ResolvedEmissionPolicy, InvalidEmissionPolicy> {
295        match self {
296            Self::Preset { profile, delta } => {
297                let policy = match EmissionPolicy::for_profile(profile).apply_delta(delta) {
298                    Ok(policy) => policy,
299                    Err(error) => return Err(error),
300                };
301                Ok(ResolvedEmissionPolicy {
302                    requested_profile: Some(profile),
303                    policy,
304                })
305            }
306            Self::Explicit(policy) => Ok(ResolvedEmissionPolicy {
307                requested_profile: None,
308                policy,
309            }),
310        }
311    }
312}
313
314impl Default for EmissionSelection {
315    fn default() -> Self {
316        SchemaProfile::Full.into()
317    }
318}
319
320impl From<SchemaProfile> for EmissionSelection {
321    fn from(profile: SchemaProfile) -> Self {
322        Self::Preset {
323            profile,
324            delta: EmissionPolicyDelta::default(),
325        }
326    }
327}
328
329impl From<EmissionPolicy> for EmissionSelection {
330    fn from(policy: EmissionPolicy) -> Self {
331        Self::Explicit(policy)
332    }
333}
334
335/// One resolved emission source used by generation and final annotation.
336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub struct ResolvedEmissionPolicy {
338    requested_profile: Option<SchemaProfile>,
339    policy: EmissionPolicy,
340}
341
342impl ResolvedEmissionPolicy {
343    /// Profile provenance retained for the final annotation.
344    #[must_use]
345    pub const fn requested_profile(self) -> Option<SchemaProfile> {
346        self.requested_profile
347    }
348
349    /// Complete policy consumed by generation.
350    #[must_use]
351    pub const fn policy(self) -> EmissionPolicy {
352        self.policy
353    }
354}
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub(crate) struct NestedGuardScope {
358    pub(crate) ancestor_segments: Vec<String>,
359    pub(crate) guards: Vec<ConditionalGuard>,
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
363pub(crate) struct GuardScopes {
364    pub(crate) outer: Vec<ConditionalGuard>,
365    pub(crate) nested: Vec<NestedGuardScope>,
366}
367
368impl GuardScopes {
369    pub(crate) fn new(outer: Vec<ConditionalGuard>, nested: Vec<NestedGuardScope>) -> Self {
370        Self { outer, nested }
371    }
372
373    pub(crate) fn is_empty(&self) -> bool {
374        self.outer.is_empty() && self.nested.is_empty()
375    }
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub(crate) struct NonEmptyGuardScopes(GuardScopes);
380
381impl NonEmptyGuardScopes {
382    pub(crate) fn new(scopes: GuardScopes) -> Option<Self> {
383        (!scopes.is_empty()).then_some(Self(scopes))
384    }
385
386    pub(crate) fn scopes(&self) -> &GuardScopes {
387        &self.0
388    }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub(crate) enum EmissionAnchor {
393    Root,
394    Local(Vec<String>),
395}
396
397impl EmissionAnchor {
398    pub(crate) fn from_segments(segments: &[String]) -> Self {
399        if segments.is_empty() {
400            Self::Root
401        } else {
402            Self::Local(segments.to_vec())
403        }
404    }
405
406    const fn is_root(&self) -> bool {
407        matches!(self, Self::Root)
408    }
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub(crate) enum ConditionalFlavor {
413    Ordinary,
414    KindPartition,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub(crate) enum TerminalWhen {
419    Always,
420    Guarded(NonEmptyGuardScopes),
421}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
424pub(crate) enum EmissionClass {
425    Mandatory,
426    Conditional {
427        guards: GuardScopes,
428        anchor: EmissionAnchor,
429        flavor: ConditionalFlavor,
430    },
431    Terminal {
432        when: TerminalWhen,
433    },
434}
435
436impl EmissionClass {
437    pub(crate) fn conditional(
438        guards: GuardScopes,
439        anchor_segments: &[String],
440        flavor: ConditionalFlavor,
441    ) -> Self {
442        if guards.is_empty() {
443            Self::Mandatory
444        } else {
445            Self::Conditional {
446                guards,
447                anchor: EmissionAnchor::from_segments(anchor_segments),
448                flavor,
449            }
450        }
451    }
452
453    pub(crate) fn terminal_guarded(guards: Vec<ConditionalGuard>) -> Option<Self> {
454        let scopes = NonEmptyGuardScopes::new(GuardScopes::new(guards, Vec::new()))?;
455        Some(Self::Terminal {
456            when: TerminalWhen::Guarded(scopes),
457        })
458    }
459
460    pub(crate) const fn terminal_always() -> Self {
461        Self::Terminal {
462            when: TerminalWhen::Always,
463        }
464    }
465
466    pub(crate) const fn kind(&self) -> EmissionClassKind {
467        match self {
468            Self::Mandatory => EmissionClassKind::Mandatory,
469            Self::Conditional {
470                anchor: EmissionAnchor::Root,
471                flavor: ConditionalFlavor::Ordinary,
472                ..
473            } => EmissionClassKind::OrdinaryRoot,
474            Self::Conditional {
475                anchor: EmissionAnchor::Local(_),
476                flavor: ConditionalFlavor::Ordinary,
477                ..
478            } => EmissionClassKind::OrdinaryLocal,
479            Self::Conditional {
480                anchor: EmissionAnchor::Root,
481                flavor: ConditionalFlavor::KindPartition,
482                ..
483            } => EmissionClassKind::KindPartitionRoot,
484            Self::Conditional {
485                anchor: EmissionAnchor::Local(_),
486                flavor: ConditionalFlavor::KindPartition,
487                ..
488            } => EmissionClassKind::KindPartitionLocal,
489            Self::Terminal {
490                when: TerminalWhen::Always,
491            } => EmissionClassKind::TerminalAlways,
492            Self::Terminal {
493                when: TerminalWhen::Guarded(_),
494            } => EmissionClassKind::TerminalGuarded,
495        }
496    }
497}
498
499/// Policy-relevant class without its guard and anchor payloads.
500#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
501pub enum EmissionClassKind {
502    /// Constraint retained by every decision-table policy.
503    Mandatory,
504    /// Ordinary conditional anchored at the document root.
505    OrdinaryRoot,
506    /// Ordinary conditional anchored below the document root.
507    OrdinaryLocal,
508    /// Kind partition anchored at the document root.
509    KindPartitionRoot,
510    /// Kind partition anchored below the document root.
511    KindPartitionLocal,
512    /// Unconditional terminating behavior.
513    TerminalAlways,
514    /// Guarded terminating behavior.
515    TerminalGuarded,
516}
517
518/// Producer category used for emission diagnostics and accounting.
519#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
520pub enum EmissionOrigin {
521    /// Guarded path evidence.
522    Overlay,
523    /// Runtime-hard contract requirement.
524    RequirementImplication,
525    /// Constraint for a lower-precedence merge layer.
526    MergeShadow,
527    /// Provider member conditionally retained by omission logic.
528    OmittedMember,
529    /// Requirement projected back from a rendered sink.
530    Backprojection,
531}