Skip to main content

tatara_process/
encapsulates.rs

1//! `EncapsulatesSpec` — how a Process relates to pre-existing
2//! in-cluster state.
3//!
4//! The substrate move: every long-running workload on a pleme-io
5//! cluster — raw HelmReleases, Flux Kustomizations, bare Deployments
6//! — becomes a Process without disruption. Three modes:
7//!
8//! * **Manage** (default) — Process IS the control loop. New
9//!   HR/Kustomization emitted by the reconciler use ownerRefs
10//!   pointing at the Process; cascade-delete on Reaped.
11//!
12//! * **Adopt** — Take over an existing HR/Kustomization in place.
13//!   Reconciler emits a new HR with `releaseName` matching the
14//!   running release; helm-controller adopts the existing release
15//!   under new management. **No pod restart**; no values diff
16//!   unless operator changes them. The original raw HR can be
17//!   deleted from git after the takeover confirms.
18//!
19//! * **Observe** — Read-only awareness. Process watches the existing
20//!   state for postcondition pillars + emits routing/exports/
21//!   attestation, but does NOT modify or own the underlying
22//!   HR/Kustomization. Useful for adding DNS + observability to
23//!   legacy stacks without taking over.
24//!
25//! These compose progressively: Observe an HR first to confirm
26//! shape, promote to Adopt for zero-downtime takeover, then to
27//! Manage once the Process drives values.
28//!
29//! Lisp authoring:
30//! ```lisp
31//! :encapsulates (:kind (:existing-helm-release
32//!                       :namespace    "demo-ns"
33//!                       :name         "demo-app"
34//!                       :release-name "demo-app-consolidated")
35//!                :mode Adopt)
36//! ```
37
38use schemars::JsonSchema;
39use serde::{Deserialize, Serialize};
40use std::collections::BTreeMap;
41use tatara_lisp::DeriveTataraDomain;
42
43/// How a Process wraps pre-existing in-cluster state.
44///
45/// Optional on `ProcessSpec` — None means the Process is greenfield
46/// (Manage mode applied to nothing pre-existing). The render phase
47/// branches on `kind` to decide whether to emit fresh resources or
48/// reference/adopt running ones.
49#[derive(DeriveTataraDomain, Clone, Debug, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "camelCase")]
51#[tatara(keyword = "defencapsulates")]
52pub struct EncapsulatesSpec {
53    /// What kind of pre-existing state.
54    pub kind: EncapsulationKind,
55
56    /// Reconciler's relationship to that state. Defaults to `Manage`
57    /// (the operational default when `encapsulates` is set without
58    /// an explicit mode).
59    #[serde(default)]
60    pub mode: EncapsulationMode,
61}
62
63impl EncapsulatesSpec {
64    /// Closed-set-driven presence probe — does this [`EncapsulatesSpec`]
65    /// carry the given [`EncapsulationMode`] discriminator on its
66    /// [`Self::mode`] slot? The ONE substrate primitive that owns the
67    /// `(EncapsulatesSpec, EncapsulationMode) -> bool` scalar-carrier walk
68    /// shape.
69    ///
70    /// # Second scalar-carrier peer on the presence-probe axis
71    ///
72    /// Peer of [`crate::spec::SignalPolicy::has_sighup_strategy`] — both
73    /// probe a scalar closed-set-discriminator field on an inner
74    /// [`crate::crd::ProcessSpec`] struct via a one-line
75    /// `self.<field> == kind` body. Together they close the SCALAR-CARRIER
76    /// stratum of the workspace-wide closed-set-driven presence-probe
77    /// algebra (the workspace-wide algebra spans three underlying
78    /// representation kinds — Option-slot, slice, scalar — see the
79    /// [`crate::spec::SignalPolicy::has_sighup_strategy`] docstring for
80    /// the full-shape rundown; this method is the second scalar-carrier
81    /// instance).
82    ///
83    /// # Semantics — VARIANT match, not POPULATED slot
84    ///
85    /// `has_mode(kind)` returns `true` iff `self.mode == kind`. On an
86    /// [`EncapsulatesSpec`] whose `mode` slot is the substrate default
87    /// ([`EncapsulationMode::default`] = [`EncapsulationMode::Manage`])
88    /// the probe returns `true` for [`EncapsulationMode::Manage`] and
89    /// `false` for every other variant — legitimate operator signal
90    /// symmetric to [`crate::signal::SighupStrategy::default`]. An
91    /// operator who opted `encapsulates` in (populating the parent
92    /// `Option<EncapsulatesSpec>`) but left `:mode` unset IS configured
93    /// for `Manage`, and a `:requires (encapsulation-mode-Manage)` check
94    /// should pass on that spec.
95    ///
96    /// # Interaction with the parent `Option` gate
97    ///
98    /// The parent `Process.spec.encapsulates` field is an
99    /// `Option<EncapsulatesSpec>` (greenfield Processes carry `None`);
100    /// downstream consumers gate this probe on the parent presence via
101    /// `spec.encapsulates.as_ref().is_some_and(|e| e.has_mode(kind))`.
102    /// A permanent Process with `encapsulates: None` returns `false` for
103    /// every kind — including the default `Manage` variant — because
104    /// the operator DECLINED the encapsulation surface entirely rather
105    /// than defaulting into it. Symmetric to the
106    /// `resolved_ephemeral().is_some_and(…)` gate the
107    /// `export-when-<kind>` / `channel-<kind>` / `report-format-<kind>` /
108    /// `artifact-<kind>` slice-level probes compose on the ephemeral
109    /// axis.
110    ///
111    /// # Compounding
112    ///
113    /// A future closed-set-discriminator scalar field on `EncapsulatesSpec`
114    /// (a hypothetical `SubmodeKind` selecting a sub-strategy inside the
115    /// Adopt/Manage modes; a `HandoffPhase` scalar selecting when the
116    /// reconciler swaps ownership) lands as ONE peer inherent method
117    /// with the same one-line `self.<field> == kind` body and routes
118    /// through the same `strip_and_classify_prefixed_kind::<K, _>` shape
119    /// in `tatara-check`. A future
120    /// [`EncapsulationMode`] variant (a hypothetical `Observe` submode,
121    /// a `Migrate` for scripted mode transitions) reaches every
122    /// downstream through ONE `ALL` entry on the closed set with the
123    /// probe body untouched.
124    ///
125    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
126    /// proofs; the scalar-carrier presence-probe body lives at ONE
127    /// substrate site so every downstream (`encapsulation-mode-<kind>`
128    /// require-tag family in `tatara-check`, closed-set audit
129    /// dispatchers, future variant additions on [`EncapsulationMode`])
130    /// binds through the SAME shape rather than restating the
131    /// `encapsulates.mode == kind` closure body at each callsite.
132    /// THEORY.md §VI.1 — generation over composition; a future
133    /// [`EncapsulationMode`] variant lands at ONE `ALL` entry + ONE
134    /// `as_str` arm on the closed set and the probe picks it up
135    /// mechanically without further per-consumer edits.
136    #[must_use]
137    pub fn has_mode(&self, kind: EncapsulationMode) -> bool {
138        self.mode == kind
139    }
140}
141
142/// Three concrete kinds the substrate knows how to wrap. Exactly-
143/// one-Option pattern matching `Intent` / `Lifetime` — additive on
144/// the wire, every variant typed.
145#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
146#[serde(rename_all = "camelCase")]
147pub struct EncapsulationKind {
148    /// An existing FluxCD HelmRelease. The reconciler emits a new HR
149    /// with the SAME `release_name` — helm-controller finds + adopts
150    /// the in-cluster release without recreating Pods.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub existing_helm_release: Option<ExistingHelmRelease>,
153
154    /// An existing FluxCD Kustomization. The reconciler stops emitting
155    /// its own and instead references the existing one.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub existing_kustomization: Option<ExistingKustomization>,
158
159    /// Pre-existing in-cluster workload (Deployment/StatefulSet/etc)
160    /// not Flux-managed. The reconciler adds ownerRefs + emits
161    /// routing only. The workload stays where it is.
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub bare_workload: Option<BareWorkload>,
164}
165
166/// Resolved enum view used by the render phase.
167#[derive(Clone, Debug)]
168pub enum EncapsulationKindVariant<'a> {
169    ExistingHelmRelease(&'a ExistingHelmRelease),
170    ExistingKustomization(&'a ExistingKustomization),
171    BareWorkload(&'a BareWorkload),
172}
173
174impl EncapsulationKindVariant<'_> {
175    /// Reverse projection — every borrowed variant knows its
176    /// [`EncapsulationTarget`] discriminator. Pairs with
177    /// [`EncapsulationTarget::select`] so
178    /// `EncapsulationTarget::select(kind).map(|v| v.target())`
179    /// round-trips the closed set on the populated side; pinned by
180    /// `encapsulation_target_round_trips_through_variant_target`.
181    /// Future target-keyed consumers (metric labels like
182    /// `tatara_encapsulations_total{target="existingHelmRelease"}`,
183    /// status reason strings, audit-trail classifiers, LSP completion
184    /// lists) reach through this projection instead of pattern-matching
185    /// the payload-carrying view.
186    pub fn target(&self) -> EncapsulationTarget {
187        match self {
188            Self::ExistingHelmRelease(_) => EncapsulationTarget::ExistingHelmRelease,
189            Self::ExistingKustomization(_) => EncapsulationTarget::ExistingKustomization,
190            Self::BareWorkload(_) => EncapsulationTarget::BareWorkload,
191        }
192    }
193}
194
195/// `EncapsulationKindVariant`'s [`crate::tagged_union::VariantKind`] impl
196/// delegates to the inherent [`Self::target`] — the substrate trait names
197/// the reverse projection uniformly across every borrowed-view enum on
198/// `ProcessSpec`'s tagged-union axis, while the inherent method's
199/// domain-specific name (`.target()`, matching `EncapsulationTarget`) stays
200/// load-bearing at every consumer site. The one-line delegation IS the
201/// only per-site restatement; the ground-truth arm-to-Kind mapping lives
202/// at the inherent method above.
203impl crate::tagged_union::VariantKind<EncapsulationTarget> for EncapsulationKindVariant<'_> {
204    fn variant_kind(&self) -> EncapsulationTarget {
205        self.target()
206    }
207}
208
209/// Closed-set discriminator over `EncapsulationKind`'s three tagged-union
210/// slots. Single source of truth that drives `EncapsulationKind::variant`'s
211/// ambiguity + emptiness resolver, the `EncapsulationKindError::Empty`
212/// diagnostic message, and the reverse `EncapsulationKindVariant::target`
213/// projection. Adding a fourth encapsulation target (e.g., a future
214/// `ExistingNamespace`, `ExistingDaemonSet`, or `ExistingService`) lands
215/// at one `ALL` entry + one `as_str` arm + one `select` arm + one
216/// `EncapsulationKindVariant::target` arm — exhaustively checked by the
217/// compiler.
218///
219/// The (open authoring surface, closed typed discriminator) split mirrors
220/// every other multi-Option tagged union on this `ProcessSpec` axis:
221/// [`crate::intent::IntentKind`] discriminates [`crate::intent::Intent`];
222/// [`crate::lifetime::LifetimeKind`] discriminates
223/// [`crate::lifetime::Lifetime`];
224/// [`crate::export::ArtifactKind`] discriminates
225/// [`crate::export::ArtifactSource`];
226/// [`crate::export::ChannelKind`] discriminates
227/// [`crate::export::VectorChannel`]. The carrier here is named
228/// `EncapsulationKind` (not `Encapsulation`) because it predates the
229/// closed-set lift convention; `EncapsulationTarget` is the typed
230/// discriminator the rest of the typescape projects through, named for
231/// the semantic role each variant plays (a target of encapsulation).
232#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
233#[closed_set(via = "as_str", generate_unknown, display)]
234pub enum EncapsulationTarget {
235    ExistingHelmRelease,
236    ExistingKustomization,
237    BareWorkload,
238}
239
240impl EncapsulationTarget {
241    /// The closed set of encapsulation targets — single source of truth
242    /// that drives `EncapsulationKind::variant`'s sweep so a variant
243    /// added without an `ALL` entry never reaches the resolver. The
244    /// `[Self; 3]` array literal forces the arity at compile time.
245    pub const ALL: [Self; 3] = [
246        Self::ExistingHelmRelease,
247        Self::ExistingKustomization,
248        Self::BareWorkload,
249    ];
250
251    /// Canonical camelCase wire-format key — matches the serde
252    /// `rename_all = "camelCase"` field name on the corresponding
253    /// `Option<…>` slot of `EncapsulationKind`. The
254    /// `EncapsulationKindError::Empty` diagnostic composes the
255    /// human-readable list from this projection so a new variant lands
256    /// in the operator-facing diagnostic automatically via the `ALL`
257    /// sweep, not via hand-maintained error-string drift. Pinned by
258    /// `encapsulation_target_as_str_matches_field_name`.
259    pub const fn as_str(self) -> &'static str {
260        match self {
261            Self::ExistingHelmRelease => "existingHelmRelease",
262            Self::ExistingKustomization => "existingKustomization",
263            Self::BareWorkload => "bareWorkload",
264        }
265    }
266
267    /// Project an `EncapsulationKind` borrow into the optional typed
268    /// variant view for this target. Returns `None` iff the matching
269    /// slot is `None`. Composes the closed-set sweep
270    /// `EncapsulationKind::variant` loops over. Mirrors
271    /// [`crate::intent::IntentKind::select`],
272    /// [`crate::lifetime::LifetimeKind::select`],
273    /// [`crate::export::ArtifactKind::select`], and
274    /// [`crate::export::ChannelKind::select`].
275    pub fn select<'a>(self, kind: &'a EncapsulationKind) -> Option<EncapsulationKindVariant<'a>> {
276        match self {
277            Self::ExistingHelmRelease => kind
278                .existing_helm_release
279                .as_ref()
280                .map(EncapsulationKindVariant::ExistingHelmRelease),
281            Self::ExistingKustomization => kind
282                .existing_kustomization
283                .as_ref()
284                .map(EncapsulationKindVariant::ExistingKustomization),
285            Self::BareWorkload => kind
286                .bare_workload
287                .as_ref()
288                .map(EncapsulationKindVariant::BareWorkload),
289        }
290    }
291}
292
293// `impl FromStr for EncapsulationTarget` + `impl tatara_lisp::ClosedSet for
294// EncapsulationTarget` + `impl std::fmt::Display for EncapsulationTarget` are
295// generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum
296// declaration above. `label` delegates to the inherent
297// `EncapsulationTarget::as_str` via `#[closed_set(via = "as_str")]` so the
298// camelCase wire-format projection stays load-bearing (matches the serde
299// `rename_all = "camelCase"` field names on `EncapsulationKind` AND the
300// `ENCAPSULATION_TARGET_LIST` slash-joined operator diagnostic verbatim)
301// while generic `T: ClosedSet` consumers reach the STABLE workspace-wide
302// name (`label`). The `display` flag emits the `f.write_str(self.as_str())`
303// delegation block at the same proc-macro site rather than a hand-rolled
304// `fmt::Display` block per implementor.
305
306// `pub struct UnknownEncapsulationTarget(pub String)` is generated by
307// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
308// on the enum declaration above. The auto-derived label `"encapsulation target"`
309// matches the prior hand-rolled `#[error("unknown encapsulation target: {0}")]`
310// verbatim — pinned generically by clause (5) of
311// `tatara_closed_set::assert_closed_set_well_formed::<EncapsulationTarget>()` (called
312// from `encapsulation_target_is_well_formed_closed_set` in the test module).
313// Symmetric to [`UnknownEncapsulationMode`], [`crate::export::UnknownArtifactKind`],
314// [`crate::export::UnknownChannelKind`], and
315// [`crate::lifetime::UnknownTeardownPolicy`].
316
317crate::declare_tagged_union_error! {
318    pub EncapsulationKindError,
319    empty = "encapsulation kind has no variant set (one of {0} required)",
320    ambiguous = "encapsulation kind has multiple variants set; exactly one required",
321}
322
323/// Slash-joined list of every `EncapsulationTarget::as_str()` — composed
324/// once at compile time so `EncapsulationKindError::Empty`'s diagnostic
325/// carries the closed-set summary without per-variant string drift.
326/// Mirrors [`crate::intent::INTENT_KIND_LIST`] /
327/// [`crate::export::ARTIFACT_KIND_LIST`] in shape; pinned by
328/// `encapsulation_kind_error_empty_lists_every_target_in_canonical_order`.
329pub(crate) const ENCAPSULATION_TARGET_LIST: &str =
330    "existingHelmRelease/existingKustomization/bareWorkload";
331
332crate::declare_tagged_union_impls! {
333    parent = EncapsulationKind,
334    kind = EncapsulationTarget,
335    variant = EncapsulationKindVariant,
336    error = EncapsulationKindError,
337    kind_list = ENCAPSULATION_TARGET_LIST,
338}
339
340/// Pointer to an existing FluxCD HelmRelease the Process wraps.
341#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
342#[serde(rename_all = "camelCase")]
343pub struct ExistingHelmRelease {
344    /// Namespace of the HelmRelease CR.
345    pub namespace: String,
346    /// Name of the HelmRelease CR.
347    pub name: String,
348    /// The `spec.releaseName` Helm used for the actual chart install.
349    /// For Adopt mode, the reconciler's emitted HR matches this so
350    /// helm-controller adopts in-place. Required because the HR's
351    /// `metadata.name` and `spec.releaseName` aren't always equal.
352    pub release_name: String,
353}
354
355/// Pointer to an existing FluxCD Kustomization the Process wraps.
356#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
357#[serde(rename_all = "camelCase")]
358pub struct ExistingKustomization {
359    pub namespace: String,
360    pub name: String,
361}
362
363/// Pointer to a bare in-cluster workload (not Flux-managed). The
364/// reconciler identifies the underlying Pods by `selector` and adds
365/// ownerRefs / routing without emitting a new HR/Kustomization.
366#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
367#[serde(rename_all = "camelCase")]
368pub struct BareWorkload {
369    /// Namespace the workload lives in.
370    pub namespace: String,
371    /// Label selector. Must match a single Deployment/StatefulSet/
372    /// DaemonSet; multiple matches are a config error.
373    pub selector: BTreeMap<String, String>,
374}
375
376/// Three modes the reconciler dispatches on at render time.
377#[derive(
378    Clone,
379    Copy,
380    Debug,
381    Default,
382    Serialize,
383    Deserialize,
384    JsonSchema,
385    PartialEq,
386    Eq,
387    Hash,
388    tatara_closed_set::DeriveClosedSet,
389)]
390#[serde(rename_all = "PascalCase")]
391#[closed_set(via = "as_str", generate_unknown, display)]
392pub enum EncapsulationMode {
393    /// **Default** — Process IS the control loop for whatever is
394    /// inside. Emitted HR/Kustomization carry the Process's
395    /// ownerRefs; cascade-delete on Reaped.
396    #[default]
397    Manage,
398
399    /// **Adopt** — Take over the existing release/kustomization in
400    /// place. New HR emitted matches the existing `releaseName`;
401    /// pods don't restart. Used during migration from raw HR → Process.
402    Adopt,
403
404    /// **Observe** — Read-only. Emit routing/exports/attestation but
405    /// don't modify the underlying HR/Kustomization. Used to add
406    /// DNS + observability to legacy stacks without taking over.
407    Observe,
408}
409
410impl EncapsulationMode {
411    /// The closed set of encapsulation modes — single source of truth
412    /// that drives the `as_str` / Display / `FromStr` triad and the
413    /// typed `emits_workload` / `preserves_release_name` dispatch.
414    /// Adding a fourth variant lands at one `ALL` entry + one `as_str`
415    /// arm + one arm in each of the two boolean projections —
416    /// exhaustively checked by the compiler (the `[Self; 3]` array
417    /// literal forces the arity).
418    ///
419    /// Sibling closed-set lifts on the same `ProcessSpec` axis:
420    /// [`crate::export::ExportTrigger::ALL`],
421    /// [`crate::lifetime::TeardownPolicy::ALL`],
422    /// [`crate::intent::IntentKind::ALL`],
423    /// [`crate::lifetime::LifetimeKind::ALL`],
424    /// [`crate::boundary::ConditionKind::ALL`],
425    /// [`crate::phase::ProcessPhase::ALL`],
426    /// [`crate::signal::ProcessSignal::ALL`].
427    pub const ALL: [Self; 3] = [Self::Manage, Self::Adopt, Self::Observe];
428
429    /// Canonical PascalCase wire-format projection — matches the serde
430    /// `rename_all = "PascalCase"` output verbatim. Used by Display
431    /// (single source of truth), by `FromStr` to identify the variant
432    /// from its annotation / status-field representation, and by
433    /// operator-facing reason strings without reaching for `{:?}` Debug
434    /// formatting. Pinned by `mode_as_str_matches_serde`.
435    pub const fn as_str(self) -> &'static str {
436        match self {
437            Self::Manage => "Manage",
438            Self::Adopt => "Adopt",
439            Self::Observe => "Observe",
440        }
441    }
442
443    /// True iff the reconciler should emit (or re-emit) the
444    /// underlying HR/Kustomization at render time.
445    /// Observe ⇒ false; Manage/Adopt ⇒ true.
446    ///
447    /// Closed-set match (not `matches!`) so adding a fourth variant
448    /// triggers the compiler's exhaustiveness check at this site
449    /// rather than silently defaulting to `false`. ONE typed dispatch
450    /// over the closed set that replaces the
451    /// `mode == EncapsulationMode::Observe` hand-rolled equality at
452    /// the reconciler's render entry — the truth table for "should
453    /// this mode emit a workload?" is now owned by the typed surface,
454    /// not by a pattern fragment two crates have to keep coherent.
455    pub const fn emits_workload(self) -> bool {
456        match self {
457            Self::Manage | Self::Adopt => true,
458            Self::Observe => false,
459        }
460    }
461
462    /// True iff the reconciler should preserve the existing release
463    /// name (so helm-controller adopts in-place). Only Adopt.
464    ///
465    /// Closed-set match (not `matches!`) so adding a fourth variant
466    /// triggers the compiler's exhaustiveness check at this site.
467    /// ONE typed dispatch that replaces the
468    /// `mode == EncapsulationMode::Adopt` hand-rolled equality at the
469    /// reconciler's `render_aplicacao` adoption-annotation branch.
470    pub const fn preserves_release_name(self) -> bool {
471        match self {
472            Self::Adopt => true,
473            Self::Manage | Self::Observe => false,
474        }
475    }
476}
477
478// `impl FromStr for EncapsulationMode` + `impl tatara_lisp::ClosedSet for
479// EncapsulationMode` + `impl std::fmt::Display for EncapsulationMode` are
480// generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum
481// declaration above. `label` delegates to the inherent
482// `EncapsulationMode::as_str` via `#[closed_set(via = "as_str")]` so the
483// PascalCase wire-format projection stays load-bearing (matches the serde
484// `rename_all = "PascalCase"` external-tag form on the wire AND the
485// reconciler's `mode: {Manage,Adopt,Observe}` status-condition reason
486// strings verbatim) while generic `T: ClosedSet` consumers reach the
487// STABLE workspace-wide name (`label`). The `display` flag emits the
488// `f.write_str(self.as_str())` delegation block at the same proc-macro
489// site rather than a hand-rolled `fmt::Display` block per implementor.
490
491// `pub struct UnknownEncapsulationMode(pub String)` is generated by
492// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
493// on the enum declaration above. The auto-derived label `"encapsulation mode"`
494// matches the prior hand-rolled `#[error("unknown encapsulation mode: {0}")]`
495// verbatim — pinned generically by clause (5) of
496// `tatara_closed_set::assert_closed_set_well_formed::<EncapsulationMode>()` (called
497// from `mode_is_well_formed_closed_set` in the test module).
498// Symmetric to [`UnknownEncapsulationTarget`], [`crate::export::UnknownExportTrigger`],
499// [`crate::lifetime::UnknownTeardownPolicy`],
500// [`crate::boundary::UnknownConditionKind`], and
501// [`crate::phase::UnknownPhase`].
502
503/// Extension trait on `Option<EncapsulatesSpec>` — the ONE substrate
504/// primitive that owns the "collapse the parent `encapsulates`
505/// Option-carrier before probing the inner spec" shape shared by every
506/// downstream require-tag family whose vocabulary reads through
507/// [`crate::crd::ProcessSpec::encapsulates`].
508///
509/// # Why lift
510///
511/// The point-domain require-tag classifier in
512/// `tatara-reconciler::bin::tatara-check` composed the SAME two-step
513/// chain (`spec.encapsulates.as_ref().is_some_and(|e| e.<probe>(k))`)
514/// at TWO consecutive rows in `evaluate_point_require_tag`'s prefix
515/// table:
516///
517/// * `encapsulation-mode-<kind>` — projects onto the
518///   [`EncapsulatesSpec::has_mode`] scalar-carrier probe.
519/// * `encapsulation-target-<kind>` — projects onto the
520///   [`crate::tagged_union::TaggedUnion::has`] presence probe over the
521///   nested [`EncapsulationKind`] tagged union.
522///
523/// Two restatements of the ONE Option-carrier collapse past the
524/// ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold — post-lift ONE
525/// substrate primitive owns the `(Option<EncapsulatesSpec>) → probe`
526/// discipline, and every current + future presence probe threaded
527/// through `spec.encapsulates` plugs into it through the SAME trait
528/// shape without a bespoke Option-arm at the caller.
529///
530/// # Semantics vs. the pre-lift chain
531///
532/// `.as_ref().is_some_and(|e| e.has_mode(k))` returns `false` on the
533/// "no encapsulation" outcome (`spec.encapsulates == None`, e.g., a
534/// greenfield Process that declined the encapsulation surface entirely)
535/// AND on the "encapsulation set but the probed axis doesn't match"
536/// outcome. The lifted [`Self::has_mode`] projection returns `false` on
537/// the "no encapsulation" outcome and delegates to the inner
538/// [`EncapsulatesSpec::has_mode`] on the "encapsulation set" outcome —
539/// observationally equivalent to the pre-lift chain at every callsite.
540/// Symmetric shape for [`Self::has_target`] over the nested
541/// [`EncapsulationKind`] tagged-union probe.
542///
543/// # Peer to [`crate::lifetime::Lifetime::ephemeral_exports`]
544///
545/// Both lifts collapse an Option-carrier at ONE substrate site so
546/// downstream presence probes on the same nested carrier compose
547/// through a uniform shape. `ephemeral_exports` returns `&[ExportSpec]`
548/// (empty slice on the collapsed arm; slice methods return `false` on
549/// empty via `.iter().any(...)`); this trait returns `bool` directly
550/// because the two probes on `EncapsulatesSpec` are not slice-shaped
551/// (a scalar-carrier equality and a tagged-union `.has()`). Both bind
552/// their respective require-tag families' probes to a `spec.<field>
553/// .<probe>(k)` shape without an `.as_ref().is_some_and(...)` chain at
554/// the callsite.
555///
556/// # Compounding
557///
558/// A future third closed-set-driven presence probe reaching through
559/// `spec.encapsulates` (a hypothetical `SubmodeKind` scalar on
560/// [`EncapsulatesSpec`], a `HandoffPhase` closed-set on a new nested
561/// carrier, a further tagged-union discriminator on
562/// [`EncapsulationKind`]) lands as ONE more method on this trait +
563/// ONE more `if let Some(res) = strip_and_classify_prefixed_kind::<K,
564/// _>(tag, "prefix-", |k| spec.encapsulates.<new-probe>(k)) { return
565/// res; }` branch in the require-tag classifier's prefix table — no
566/// per-caller `.as_ref().is_some_and(...)` restatement, no per-caller
567/// `spec.encapsulates.as_ref()` walk. A future diagnostic shift on the
568/// Option-carrier collapse (surfacing "encapsulation declined" as a
569/// distinct near-miss from "encapsulation set but axis absent") lands
570/// at THIS ONE substrate owner and every current + future require-tag
571/// family inherits the shift by construction.
572///
573/// Theory anchor: THEORY.md §II.1 invariant 5 (composition preserves
574/// proofs — the Option-carrier collapse lives at ONE substrate site so
575/// every downstream `encapsulation-<axis>` require-tag family binds
576/// through the SAME shape). THEORY.md §VI.1 (generation over
577/// composition — a new probe on `EncapsulatesSpec` reaches this trait
578/// through a peer method without a bespoke Option-arm at the caller).
579///
580/// Pinned by
581/// [`tests::encapsulates_option_ext_has_mode_returns_false_on_none_for_every_kind`],
582/// [`tests::encapsulates_option_ext_has_mode_matches_inner_probe_when_present`],
583/// [`tests::encapsulates_option_ext_has_target_returns_false_on_none_for_every_target`],
584/// and
585/// [`tests::encapsulates_option_ext_has_target_matches_inner_kind_probe_when_present`].
586pub trait EncapsulatesSpecOptionExt {
587    /// True iff this `Option<EncapsulatesSpec>` is `Some(e)` AND
588    /// [`EncapsulatesSpec::has_mode`] on the inner spec answers `true`
589    /// for the given [`EncapsulationMode`]. Returns `false` on `None`
590    /// (a greenfield Process that declined encapsulation entirely) —
591    /// including for the default [`EncapsulationMode::Manage`],
592    /// because the operator DECLINED the encapsulation surface rather
593    /// than defaulting into it.
594    fn has_mode(&self, kind: EncapsulationMode) -> bool;
595
596    /// True iff this `Option<EncapsulatesSpec>` is `Some(e)` AND the
597    /// inner [`EncapsulatesSpec::kind`] tagged union carries a
598    /// populated slot addressed by the given [`EncapsulationTarget`].
599    /// Returns `false` on `None`.
600    fn has_target(&self, kind: EncapsulationTarget) -> bool;
601}
602
603impl EncapsulatesSpecOptionExt for Option<EncapsulatesSpec> {
604    fn has_mode(&self, kind: EncapsulationMode) -> bool {
605        self.as_ref().is_some_and(|e| e.has_mode(kind))
606    }
607
608    fn has_target(&self, kind: EncapsulationTarget) -> bool {
609        self.as_ref().is_some_and(|e| e.kind.has(kind))
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    fn demo_adopt() -> EncapsulatesSpec {
618        EncapsulatesSpec {
619            kind: EncapsulationKind {
620                existing_helm_release: Some(ExistingHelmRelease {
621                    namespace: "demo-ns".into(),
622                    name: "demo-app".into(),
623                    release_name: "demo-app-consolidated".into(),
624                }),
625                ..EncapsulationKind::default()
626            },
627            mode: EncapsulationMode::Adopt,
628        }
629    }
630
631    #[test]
632    fn kind_empty_errors() {
633        let k = EncapsulationKind::default();
634        assert_eq!(
635            k.variant().unwrap_err(),
636            EncapsulationKindError::Empty(ENCAPSULATION_TARGET_LIST)
637        );
638    }
639
640    #[test]
641    fn kind_existing_hr_resolves() {
642        let s = demo_adopt();
643        match s.kind.variant().unwrap() {
644            EncapsulationKindVariant::ExistingHelmRelease(h) => {
645                assert_eq!(h.namespace, "demo-ns");
646                assert_eq!(h.release_name, "demo-app-consolidated");
647            }
648            other => panic!("expected ExistingHelmRelease, got {other:?}"),
649        }
650    }
651
652    #[test]
653    fn kind_two_variants_ambiguous() {
654        let k = EncapsulationKind {
655            existing_helm_release: Some(ExistingHelmRelease {
656                namespace: "ns".into(),
657                name: "n".into(),
658                release_name: "r".into(),
659            }),
660            existing_kustomization: Some(ExistingKustomization {
661                namespace: "ns".into(),
662                name: "n".into(),
663            }),
664            ..EncapsulationKind::default()
665        };
666        assert_eq!(k.variant().unwrap_err(), EncapsulationKindError::Ambiguous);
667    }
668
669    #[test]
670    fn mode_dispatch() {
671        assert!(EncapsulationMode::Manage.emits_workload());
672        assert!(EncapsulationMode::Adopt.emits_workload());
673        assert!(!EncapsulationMode::Observe.emits_workload());
674
675        assert!(!EncapsulationMode::Manage.preserves_release_name());
676        assert!(EncapsulationMode::Adopt.preserves_release_name());
677        assert!(!EncapsulationMode::Observe.preserves_release_name());
678    }
679
680    #[test]
681    fn mode_default_is_manage() {
682        assert_eq!(EncapsulationMode::default(), EncapsulationMode::Manage);
683    }
684
685    // ── scalar-carrier presence probe on EncapsulatesSpec × EncapsulationMode ─
686    //
687    // Fail-before-pass-after granularity: [`EncapsulatesSpec::has_mode`]
688    // did not exist before this commit — every consumer of the
689    // `(EncapsulatesSpec, EncapsulationMode) -> bool` scalar-carrier
690    // probe shape restated the `encapsulates.mode == kind` closure body
691    // at its own callsite. Post-lift the shape lives at ONE substrate
692    // owner and every downstream (the `encapsulation-mode-<kind>`
693    // require-tag family in `tatara-check`, future audit dispatchers
694    // walking [`EncapsulationMode::ALL`], any future CRD-facing closed-
695    // set discriminator on a scalar `EncapsulatesSpec` field) binds
696    // through the SAME `has(kind)` shape the Option-slot (Intent::has,
697    // Lifetime::has), slice-level (ConditionSliceExt::has_kind,
698    // DependsOnSliceExt::has_must_reach, ComplianceBindingSliceExt::
699    // has_verification_phase, ExportSpecSliceExt::{has_when,
700    // has_channel_kind, has_report_format, has_artifact_kind}) and
701    // sister scalar-carrier ([`crate::spec::SignalPolicy::has_sighup_strategy`])
702    // primitives publish.
703
704    /// DIAGONAL — for every [`EncapsulationMode`] variant, an
705    /// [`EncapsulatesSpec`] whose `mode` field is set to that variant
706    /// returns `true` from `has_mode` on that same variant AND `false`
707    /// on every other variant. Sweep the [`EncapsulationMode::ALL`] ×
708    /// ALL cross so a regression that hard-coded the arm to a single
709    /// variant (silently returning `true` on every populated spec
710    /// regardless of query kind) or wired the equality to a fixed
711    /// unrelated field (a stray probe on `kind` — the tagged-union
712    /// carrier — instead of `mode`) fails HERE at the substrate
713    /// primitive before landing at the operator-facing checks.lisp
714    /// surface.
715    #[test]
716    fn encapsulates_spec_has_mode_returns_true_iff_variant_matches() {
717        for populated in EncapsulationMode::ALL {
718            let spec = EncapsulatesSpec {
719                kind: EncapsulationKind::default(),
720                mode: populated,
721            };
722            for query in EncapsulationMode::ALL {
723                assert_eq!(
724                    spec.has_mode(query),
725                    query == populated,
726                    "mode={populated:?}: query {query:?} classification drifted",
727                );
728            }
729        }
730    }
731
732    /// DEFAULT — an [`EncapsulatesSpec`] constructed with the
733    /// [`EncapsulationMode::default`] variant carries
734    /// `mode: EncapsulationMode::Manage`, so the scalar-carrier probe
735    /// returns `true` on [`EncapsulationMode::Manage`] and `false` on
736    /// every other variant. Symmetric to the
737    /// [`crate::spec::SignalPolicy::has_sighup_strategy`] default pin —
738    /// both scalar-carrier peers publish the SAME "default is a
739    /// legitimate operator answer" contract at the substrate boundary,
740    /// distinct from the Option-slot axis where a default carrier
741    /// returns `false` for EVERY kind.
742    #[test]
743    fn encapsulates_spec_has_mode_default_probes_manage_only() {
744        let spec = EncapsulatesSpec {
745            kind: EncapsulationKind::default(),
746            mode: EncapsulationMode::default(),
747        };
748        for kind in EncapsulationMode::ALL {
749            let expected = kind == EncapsulationMode::Manage;
750            assert_eq!(
751                spec.has_mode(kind),
752                expected,
753                "default spec (mode=Manage) must return {expected} for {kind:?}",
754            );
755        }
756    }
757
758    // ── closed-set algebra for EncapsulationMode (ALL × as_str ×
759    //    Display × FromStr × emits_workload × preserves_release_name) ─
760
761    /// Structural well-formedness of [`EncapsulationMode`] as a
762    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
763    /// testkit lift that pins all three structural invariants (`ALL`
764    /// is non-empty, every variant round-trips through
765    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
766    /// outside the closed set) at ONE call site. Replaces the hand-
767    /// derived `mode_all_is_unique_and_complete` +
768    /// `mode_roundtrip_via_as_str` + the empty-input arm of
769    /// `unknown_encapsulation_mode_errors`. `FromStr` delegates to
770    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
771    /// exercises the same code path the reconciler hits when parsing a
772    /// CRD `enum:`-validated `mode` value back to the typed mode.
773    #[test]
774    fn mode_is_well_formed_closed_set() {
775        tatara_closed_set::assert_closed_set_well_formed::<EncapsulationMode>();
776    }
777
778    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
779    /// output verbatim for every variant. A future variant rename
780    /// (or an `as_str` arm typo) lands here at one site, instead of
781    /// drifting between the typed surface and the YAML wire format
782    /// the reconciler / operator both read.
783    #[test]
784    fn mode_as_str_matches_serde() {
785        crate::tagged_union::assert_label_matches_serde_serialization::<EncapsulationMode>();
786    }
787
788    /// The Display impl IS `as_str` — pinning this lets future callers
789    /// reach for either projection without drift. If a reviewer
790    /// accidentally re-introduces an inline match in Display, this
791    /// test would fail the moment a variant rename touches one site
792    /// but not the other.
793    #[test]
794    fn mode_display_matches_as_str() {
795        crate::tagged_union::assert_display_matches_label::<EncapsulationMode>();
796    }
797
798    /// `FromStr` rejects strings that aren't in the canonical
799    /// projection — lowercased / typo / unrelated — and the error
800    /// echoes the input verbatim so the operator-facing diagnostic
801    /// carries the offending value, not a normalized form. The
802    /// empty-input arm is pinned by
803    /// [`mode_is_well_formed_closed_set`] via the
804    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
805    /// verbatim-echo contract on the [`UnknownEncapsulationMode`]
806    /// newtype, which the trait's `make_unknown` can't see.
807    #[test]
808    fn unknown_encapsulation_mode_errors() {
809        use std::str::FromStr;
810        for bad in ["manage", "ADOPT", "Observed", "Wrap"] {
811            let err = EncapsulationMode::from_str(bad).unwrap_err();
812            assert_eq!(err.0, bad, "error payload should echo input verbatim");
813        }
814    }
815
816    /// TRUTH-TABLE CONTRACT: `emits_workload` / `preserves_release_name`
817    /// agree with the documented (mode) -> (bool, bool) table for every
818    /// variant. A new variant in `EncapsulationMode` without extending
819    /// either projection's match is caught by the compiler (closed-set
820    /// match in each method); adding a variant without extending its
821    /// truth row is caught here.
822    #[test]
823    fn mode_projection_truth_table() {
824        let table: &[(EncapsulationMode, bool, bool)] = &[
825            // (mode, emits_workload, preserves_release_name)
826            (EncapsulationMode::Manage, true, false),
827            (EncapsulationMode::Adopt, true, true),
828            (EncapsulationMode::Observe, false, false),
829        ];
830        assert_eq!(table.len(), EncapsulationMode::ALL.len());
831        for (mode, emits, preserves) in table {
832            assert_eq!(
833                mode.emits_workload(),
834                *emits,
835                "emits_workload drift for {mode:?}"
836            );
837            assert_eq!(
838                mode.preserves_release_name(),
839                *preserves,
840                "preserves_release_name drift for {mode:?}"
841            );
842        }
843    }
844
845    /// DRIFT-PROOF CONTRACT: the hand-rolled
846    /// `mode == EncapsulationMode::Observe` and
847    /// `mode == EncapsulationMode::Adopt` checks the reconciler's
848    /// `render` function used pre-lift agree with the typed
849    /// projections for every variant in `ALL`. A regression that
850    /// re-introduces a raw `==` against a variant name fails here:
851    /// `!emits_workload()` IS "Observe mode" and
852    /// `preserves_release_name()` IS "Adopt mode", expressed as a
853    /// property of the typed surface rather than a pattern fragment
854    /// two crates have to keep coherent.
855    #[test]
856    fn mode_typed_projections_replace_raw_equality() {
857        for mode in EncapsulationMode::ALL {
858            assert_eq!(
859                !mode.emits_workload(),
860                mode == EncapsulationMode::Observe,
861                "!emits_workload() drift for {mode:?}"
862            );
863            assert_eq!(
864                mode.preserves_release_name(),
865                mode == EncapsulationMode::Adopt,
866                "preserves_release_name() drift for {mode:?}"
867            );
868        }
869    }
870
871    #[test]
872    fn serde_round_trip_via_yaml() {
873        let s = demo_adopt();
874        let yaml = serde_yaml::to_string(&s).unwrap();
875        assert!(yaml.contains("existingHelmRelease:"));
876        assert!(yaml.contains("releaseName: demo-app-consolidated"));
877        assert!(yaml.contains("mode: Adopt"));
878        let back: EncapsulatesSpec = serde_yaml::from_str(&yaml).unwrap();
879        assert!(back.kind.existing_helm_release.is_some());
880        assert_eq!(back.mode, EncapsulationMode::Adopt);
881    }
882
883    #[test]
884    fn bare_workload_selector_round_trips() {
885        let mut sel = BTreeMap::new();
886        sel.insert("app".into(), "demo-app".into());
887        sel.insert("tier".into(), "prod".into());
888        let s = EncapsulatesSpec {
889            kind: EncapsulationKind {
890                bare_workload: Some(BareWorkload {
891                    namespace: "legacy".into(),
892                    selector: sel,
893                }),
894                ..EncapsulationKind::default()
895            },
896            mode: EncapsulationMode::Observe,
897        };
898        let yaml = serde_yaml::to_string(&s).unwrap();
899        assert!(yaml.contains("bareWorkload:"));
900        assert!(yaml.contains("app: demo-app"));
901        assert!(yaml.contains("mode: Observe"));
902        let back: EncapsulatesSpec = serde_yaml::from_str(&yaml).unwrap();
903        match back.kind.variant().unwrap() {
904            EncapsulationKindVariant::BareWorkload(b) => {
905                assert_eq!(b.selector.len(), 2);
906                assert_eq!(b.selector.get("app").map(String::as_str), Some("demo-app"));
907            }
908            other => panic!("expected BareWorkload, got {other:?}"),
909        }
910    }
911
912    #[test]
913    fn lisp_round_trip_existing_hr() {
914        let src = r#"
915            (defencapsulates demo-adopt
916              :kind (:existing-helm-release
917                     (:namespace    "demo-ns"
918                      :name         "demo-app"
919                      :release-name "demo-app-consolidated"))
920              :mode Adopt)
921        "#;
922        let defs: Vec<tatara_lisp::NamedDefinition<EncapsulatesSpec>> =
923            tatara_lisp::compile_named::<EncapsulatesSpec>(src).expect("compile");
924        let d = &defs[0];
925        assert_eq!(d.name, "demo-adopt");
926        assert_eq!(d.spec.mode, EncapsulationMode::Adopt);
927        let h = d.spec.kind.existing_helm_release.as_ref().unwrap();
928        assert_eq!(h.namespace, "demo-ns");
929        assert_eq!(h.release_name, "demo-app-consolidated");
930    }
931
932    #[test]
933    fn lisp_default_mode_is_manage() {
934        // `:mode` omitted ⇒ Manage (Default derive).
935        let src = r#"
936            (defencapsulates greenfield
937              :kind (:existing-kustomization
938                     (:namespace "flux-system"
939                      :name      "openclaw")))
940        "#;
941        let defs: Vec<tatara_lisp::NamedDefinition<EncapsulatesSpec>> =
942            tatara_lisp::compile_named::<EncapsulatesSpec>(src).expect("compile");
943        let d = &defs[0];
944        assert_eq!(d.spec.mode, EncapsulationMode::Manage);
945    }
946
947    // ── closed-set algebra for EncapsulationTarget (ALL × as_str ×
948    //    Display × FromStr × select × EncapsulationKindVariant::target) ─
949
950    /// Construct an `EncapsulationKind` with one slot populated — the
951    /// composable construction table the closed-set property tests
952    /// loop over. Mirrors `single_slot_source` in
953    /// [`crate::export`] in shape.
954    fn single_slot_kind(target: EncapsulationTarget) -> EncapsulationKind {
955        match target {
956            EncapsulationTarget::ExistingHelmRelease => EncapsulationKind {
957                existing_helm_release: Some(ExistingHelmRelease {
958                    namespace: "ns".into(),
959                    name: "hr".into(),
960                    release_name: "rel".into(),
961                }),
962                ..EncapsulationKind::default()
963            },
964            EncapsulationTarget::ExistingKustomization => EncapsulationKind {
965                existing_kustomization: Some(ExistingKustomization {
966                    namespace: "ns".into(),
967                    name: "ks".into(),
968                }),
969                ..EncapsulationKind::default()
970            },
971            EncapsulationTarget::BareWorkload => {
972                let mut sel = BTreeMap::new();
973                sel.insert("app".into(), "x".into());
974                EncapsulationKind {
975                    bare_workload: Some(BareWorkload {
976                        namespace: "ns".into(),
977                        selector: sel,
978                    }),
979                    ..EncapsulationKind::default()
980                }
981            }
982        }
983    }
984
985    /// Construct an `EncapsulationKind` with two slots populated — drives
986    /// the pairwise `Ambiguous` sweep. Composes the single-slot
987    /// constructor on top of itself to keep one source of truth for
988    /// per-variant inner payloads.
989    fn two_slot_kind(a: EncapsulationTarget, b: EncapsulationTarget) -> EncapsulationKind {
990        let ka = single_slot_kind(a);
991        let kb = single_slot_kind(b);
992        EncapsulationKind {
993            existing_helm_release: ka.existing_helm_release.or(kb.existing_helm_release),
994            existing_kustomization: ka.existing_kustomization.or(kb.existing_kustomization),
995            bare_workload: ka.bare_workload.or(kb.bare_workload),
996        }
997    }
998
999    /// Structural well-formedness of [`EncapsulationTarget`] as a
1000    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1001    /// testkit lift that pins all three structural invariants (`ALL`
1002    /// is non-empty, every variant round-trips through
1003    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1004    /// outside the closed set) at ONE call site. Replaces the hand-
1005    /// derived `encapsulation_target_all_is_unique_and_complete` +
1006    /// `encapsulation_target_roundtrip_via_as_str` + the empty-input
1007    /// arm of `unknown_encapsulation_target_errors`. `FromStr`
1008    /// delegates to `<Self as tatara_closed_set::ClosedSet>::parse_label`, so
1009    /// this helper exercises the same code path the
1010    /// `EncapsulationKind::variant` resolver hits when keying on a
1011    /// camelCase target name back to the typed target.
1012    #[test]
1013    fn encapsulation_target_is_well_formed_closed_set() {
1014        tatara_closed_set::assert_closed_set_well_formed::<EncapsulationTarget>();
1015    }
1016
1017    /// CANONICAL-KEY CONTRACT: every `EncapsulationTarget::as_str()`
1018    /// matches the serde `rename_all = "camelCase"` field name on the
1019    /// corresponding `Option<…>` slot of `EncapsulationKind`. A future
1020    /// rename of either the struct field OR the `as_str` arm lands here
1021    /// at one site, instead of drifting between the typed surface, the
1022    /// wire format, and the `EncapsulationKindError::Empty` diagnostic.
1023    ///
1024    /// Routes through the substrate primitive
1025    /// [`crate::tagged_union::assert_single_slot_key_matches_label`],
1026    /// which pins the exactly-one-key + name-equality projection
1027    /// byte-identically for every `<T: TaggedUnion + Serialize>`
1028    /// implementor — the wire-alignment testkit shared with the sibling
1029    /// `intent_kind_as_str_matches_intent_field_name` /
1030    /// `artifact_kind_as_str_matches_field_name` /
1031    /// `channel_kind_as_str_matches_field_name` sites. Pre-lift this
1032    /// site restated a weaker YAML-substring check (`yaml.contains(&format!("{key}:"))`)
1033    /// which would silently pass on drift where a non-tagged-union
1034    /// field was added to `EncapsulationKind`; post-lift the primitive's
1035    /// JSON exactly-one form catches that drift too — at ONE substrate
1036    /// site.
1037    #[test]
1038    fn encapsulation_target_as_str_matches_field_name() {
1039        crate::tagged_union::assert_single_slot_key_matches_label::<EncapsulationKind, _>(
1040            single_slot_kind,
1041        );
1042    }
1043
1044    /// CANONICAL-NAMES PIN: byte-exact camelCase wire-format pin —
1045    /// renaming any of these strings IS a wire-format break that fails
1046    /// this test FIRST so the rename stays a deliberate decision, not a
1047    /// typo. Locks the (variant → operator-facing key) table.
1048    #[test]
1049    fn encapsulation_target_canonical_names_pinned() {
1050        assert_eq!(
1051            EncapsulationTarget::ExistingHelmRelease.as_str(),
1052            "existingHelmRelease"
1053        );
1054        assert_eq!(
1055            EncapsulationTarget::ExistingKustomization.as_str(),
1056            "existingKustomization"
1057        );
1058        assert_eq!(EncapsulationTarget::BareWorkload.as_str(), "bareWorkload");
1059    }
1060
1061    /// The Display impl IS `as_str` — pinning this lets future callers
1062    /// reach for either projection without drift. If a reviewer
1063    /// accidentally re-introduces an inline match in Display, this test
1064    /// would fail the moment a variant rename touches one site but not
1065    /// the other.
1066    #[test]
1067    fn encapsulation_target_display_matches_as_str() {
1068        crate::tagged_union::assert_display_matches_label::<EncapsulationTarget>();
1069    }
1070
1071    /// `FromStr` rejects strings that aren't in the canonical projection
1072    /// — PascalCased / typo / cross-axis-leaked inputs from sibling
1073    /// closed-set enums on the same `ProcessSpec` axis (`Manage`,
1074    /// `Adopt`, `Observe`, `OnAttested`, …) — and the error echoes the
1075    /// input verbatim so the operator-facing diagnostic carries the
1076    /// offending value, not a normalized form. `EncapsulationTarget`
1077    /// is its own axis, NOT a transparent reflection of any sibling.
1078    /// The empty-input arm is pinned by
1079    /// [`encapsulation_target_is_well_formed_closed_set`] via the
1080    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1081    /// verbatim-echo contract on the [`UnknownEncapsulationTarget`]
1082    /// newtype, which the trait's `make_unknown` can't see.
1083    #[test]
1084    fn unknown_encapsulation_target_errors() {
1085        use std::str::FromStr;
1086        for bad in [
1087            "ExistingHelmRelease",
1088            "existing_helm_release",
1089            "EXISTINGHELMRELEASE",
1090            "helmRelease",
1091            "kustomization",
1092            "Manage",
1093            "Adopt",
1094            "Observe",
1095            "OnAttested",
1096        ] {
1097            let err = EncapsulationTarget::from_str(bad).unwrap_err();
1098            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1099        }
1100    }
1101
1102    /// ROUND-TRIP CONTRACT: every target reaches its borrowed-variant
1103    /// view via `select`, and that variant projects back to the same
1104    /// target via `EncapsulationKindVariant::target`. A regression that
1105    /// misroutes a `select` arm (e.g.
1106    /// `Self::ExistingHelmRelease => kind.existing_kustomization
1107    /// .as_ref()...`) fails loudly here. Also pins that the resolver
1108    /// lands on the same target.
1109    ///
1110    /// Routes through the substrate primitive
1111    /// [`crate::tagged_union::assert_variant_round_trip`] shared with
1112    /// the sibling `intent_kind_round_trips_through_variant_kind` /
1113    /// `artifact_kind_round_trips_through_variant_kind` /
1114    /// `channel_kind_round_trips_through_variant_kind` sites — the
1115    /// projection lives at ONE substrate primitive and every site
1116    /// binds through a single call. The `target()` inherent method
1117    /// (semantic-specific to `EncapsulationTarget`) stays load-bearing
1118    /// on the callsite convention while the trait projection carries
1119    /// the round-trip check uniformly.
1120    #[test]
1121    fn encapsulation_target_round_trips_through_variant_target() {
1122        crate::tagged_union::assert_variant_round_trip::<EncapsulationKind, _>(single_slot_kind);
1123    }
1124
1125    /// SELECT-EMPTY CONTRACT: an unpopulated slot returns `None` from
1126    /// `select`, for every target. Pairs with the resolver's `Empty`
1127    /// path so a future target's slot defaulting wrong (e.g.
1128    /// accidentally `Some(Default::default())` instead of `None`) is
1129    /// caught here.
1130    #[test]
1131    fn encapsulation_target_select_returns_none_for_unset_slot() {
1132        let empty = EncapsulationKind::default();
1133        for t in EncapsulationTarget::ALL {
1134            assert!(
1135                t.select(&empty).is_none(),
1136                "{t:?} reported populated on a default EncapsulationKind"
1137            );
1138        }
1139    }
1140
1141    /// EMPTY-DIAGNOSTIC CONTRACT: the closed-set target list embedded
1142    /// in `EncapsulationKindError::Empty` echoes the canonical join of
1143    /// every `EncapsulationTarget::as_str()` projection. A variant
1144    /// added without updating `ENCAPSULATION_TARGET_LIST` (or a renamed
1145    /// variant) shows up here as a mismatch. Routes through the
1146    /// substrate primitive
1147    /// [`crate::tagged_union::assert_kind_list_matches_closed_set`]
1148    /// shared with the sibling `intent_error_empty_lists_every_kind_in_canonical_order`
1149    /// / `artifact_error_empty_lists_every_kind_in_canonical_order`
1150    /// / `channel_error_empty_lists_every_kind_in_canonical_order`
1151    /// sites — the projection lives at ONE substrate primitive and
1152    /// every site binds through a single call. The paired assertion
1153    /// that the diagnostic reaches operator-facing output verbatim
1154    /// stays local because the empty-arm construction differs per
1155    /// carrier.
1156    #[test]
1157    fn encapsulation_kind_error_empty_lists_every_target_in_canonical_order() {
1158        crate::tagged_union::assert_kind_list_matches_closed_set::<EncapsulationKind>();
1159        // And the diagnostic carries that exact list.
1160        let err = EncapsulationKind::default().variant().unwrap_err();
1161        assert_eq!(
1162            err,
1163            EncapsulationKindError::Empty(ENCAPSULATION_TARGET_LIST)
1164        );
1165    }
1166
1167    /// AMBIGUOUS-PATH CONTRACT: when two slots are populated the
1168    /// resolver yields `Ambiguous`, exhaustively across every pair in
1169    /// `ALL × ALL` (excluding the diagonal). A future asymmetry where
1170    /// one slot would silently shadow another (e.g. an `if-let` chain
1171    /// re-introducing first-wins ordering) is caught here. Routes
1172    /// through the substrate primitive
1173    /// [`crate::tagged_union::assert_two_slots_ambiguous`] shared with
1174    /// the sibling `artifact_source_two_slots_is_ambiguous_across_every_pair`
1175    /// / `vector_channel_two_slots_is_ambiguous_across_every_pair`
1176    /// / `intent_two_slots_is_ambiguous_across_every_pair` sites — the
1177    /// nested-`for a in K::ALL { for b in K::ALL { … } }` sweep lives
1178    /// at ONE substrate site.
1179    #[test]
1180    fn encapsulation_kind_two_slots_is_ambiguous_across_every_pair() {
1181        crate::tagged_union::assert_two_slots_ambiguous::<EncapsulationKind, _>(two_slot_kind);
1182    }
1183
1184    // ── EncapsulatesSpecOptionExt — Option-carrier collapse contract ──
1185
1186    /// COLLAPSED-NONE CONTRACT (mode axis): a `None` outer Option
1187    /// carries no encapsulation surface, so
1188    /// [`EncapsulatesSpecOptionExt::has_mode`] returns `false` for
1189    /// every [`EncapsulationMode`] — INCLUDING the substrate default
1190    /// [`EncapsulationMode::Manage`]. An operator who declined the
1191    /// encapsulation surface entirely is NOT configured for `Manage`;
1192    /// the default only fires when the parent Option is `Some(_)` and
1193    /// the inner `mode` slot is unset. Pre-lift the require-tag
1194    /// classifier restated `spec.encapsulates.as_ref().is_some_and(|e|
1195    /// e.has_mode(k))` inline; post-lift the collapse is a peer to
1196    /// [`crate::lifetime::Lifetime::ephemeral_exports`] (both own the
1197    /// Option-carrier arm at ONE substrate site).
1198    #[test]
1199    fn encapsulates_option_ext_has_mode_returns_false_on_none_for_every_kind() {
1200        let opt: Option<EncapsulatesSpec> = None;
1201        for m in EncapsulationMode::ALL {
1202            assert!(
1203                !opt.has_mode(m),
1204                "None carrier reported has_mode({m:?}) = true; \
1205                 the Option-carrier collapse arm must return false \
1206                 for every EncapsulationMode kind, including the \
1207                 substrate default"
1208            );
1209        }
1210    }
1211
1212    /// DELEGATED-SOME CONTRACT (mode axis): a `Some(e)` outer Option
1213    /// forwards to [`EncapsulatesSpec::has_mode`] byte-identically.
1214    /// This test pins that the extension trait's projection on the
1215    /// populated arm equals the inner-spec probe's answer for every
1216    /// [`EncapsulationMode`] kind on a spec whose `mode` slot is set
1217    /// to a specific variant — so the collapse-arm's `false` on `None`
1218    /// is the ONLY behavioral change introduced by the lift.
1219    #[test]
1220    fn encapsulates_option_ext_has_mode_matches_inner_probe_when_present() {
1221        for set_mode in EncapsulationMode::ALL {
1222            let inner = EncapsulatesSpec {
1223                kind: single_slot_kind(EncapsulationTarget::ExistingHelmRelease),
1224                mode: set_mode,
1225            };
1226            let opt: Option<EncapsulatesSpec> = Some(inner.clone());
1227            for probe in EncapsulationMode::ALL {
1228                assert_eq!(
1229                    opt.has_mode(probe),
1230                    inner.has_mode(probe),
1231                    "Some-arm projection diverged from inner probe: \
1232                     set_mode={set_mode:?} probe={probe:?}"
1233                );
1234            }
1235        }
1236    }
1237
1238    /// COLLAPSED-NONE CONTRACT (target axis): a `None` outer Option
1239    /// carries no encapsulation surface, so
1240    /// [`EncapsulatesSpecOptionExt::has_target`] returns `false` for
1241    /// every [`EncapsulationTarget`]. Symmetric to the mode-axis
1242    /// collapse; the two axes route through the SAME Option-carrier
1243    /// arm so this test AND the mode-axis peer must both stay `false`
1244    /// on `None` — a regression that leaked `true` on one axis would
1245    /// diverge the two require-tag families' truth tables.
1246    #[test]
1247    fn encapsulates_option_ext_has_target_returns_false_on_none_for_every_target() {
1248        let opt: Option<EncapsulatesSpec> = None;
1249        for t in EncapsulationTarget::ALL {
1250            assert!(
1251                !opt.has_target(t),
1252                "None carrier reported has_target({t:?}) = true; \
1253                 the Option-carrier collapse arm must return false \
1254                 for every EncapsulationTarget"
1255            );
1256        }
1257    }
1258
1259    /// DELEGATED-SOME CONTRACT (target axis): a `Some(e)` outer Option
1260    /// forwards to the inner
1261    /// [`crate::tagged_union::TaggedUnion::has`] presence probe over
1262    /// [`EncapsulatesSpec::kind`] byte-identically. Pins that populated
1263    /// slots answer `true` on their own target and `false` on the
1264    /// other two — the `select`-populated arm of the closed-set sweep
1265    /// is what the collapsed-`Some` path delegates to.
1266    #[test]
1267    fn encapsulates_option_ext_has_target_matches_inner_kind_probe_when_present() {
1268        for set_target in EncapsulationTarget::ALL {
1269            let inner = EncapsulatesSpec {
1270                kind: single_slot_kind(set_target),
1271                mode: EncapsulationMode::default(),
1272            };
1273            let opt: Option<EncapsulatesSpec> = Some(inner.clone());
1274            for probe in EncapsulationTarget::ALL {
1275                let expected = probe == set_target;
1276                assert_eq!(
1277                    opt.has_target(probe),
1278                    expected,
1279                    "Some-arm target projection wrong: \
1280                     set_target={set_target:?} probe={probe:?}"
1281                );
1282                assert_eq!(
1283                    opt.has_target(probe),
1284                    inner.kind.has(probe),
1285                    "Some-arm target projection diverged from inner \
1286                     tagged-union probe: set_target={set_target:?} \
1287                     probe={probe:?}"
1288                );
1289            }
1290        }
1291    }
1292
1293    // Per-implementor `unknown_X_message_matches_substrate_convention`
1294    // tests removed — clause (5) of
1295    // `tatara_closed_set::assert_closed_set_well_formed::<T>()` now verifies
1296    // the substrate-wide `"unknown {SET_LABEL}: {input}"` carrier shape
1297    // generically (called above on `EncapsulationTarget` /
1298    // `EncapsulationMode` through their `*_is_well_formed_closed_set`
1299    // sites). The `SET_LABEL` projection is pinned independently by
1300    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests` —
1301    // together the two contracts guarantee the operator-facing
1302    // diagnostic without needing per-enum literal pins.
1303}