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}