Skip to main content

tatara_process/
export.rs

1//! `ExportSpec` — what an ephemeral Process is allowed to leave behind.
2//!
3//! The compounding move: ephemeral envs default to **leaving nothing
4//! behind**. Anything that must survive teardown is named explicitly via
5//! one or more `ExportSpec`s on `:lifetime (:ephemeral … :exports …)`.
6//! Each export declares:
7//!
8//!   * what artifact to ship out (a [`ArtifactSource`] variant)
9//!   * where to ship it through (a [`VectorChannel`] variant — Vector
10//!     ingest endpoint, NATS JetStream subject, or stdout)
11//!   * when to ship it (an [`ExportTrigger`] variant)
12//!
13//! The pleme-io convention is that **everything emitted from a
14//! workload flows through the Vector + NATS layer**, never to ad-hoc
15//! sinks. `VectorChannel` enforces that at the type level — there is
16//! no `S3Bucket` or `RawFileSystem` variant. Vector's downstream
17//! sink graph (file, VictoriaLogs, VictoriaMetrics, Loki, …) handles
18//! durability + analytics; this primitive only names the *ingestion*
19//! shape.
20//!
21//! The reconciler's `Releasing` phase (between `Attested`/`Failed` and
22//! `Exiting`) reads `lifetime.ephemeral.exports`, filters by
23//! [`ExportTrigger`] against the terminal phase, and emits one
24//! tatara-export-worker Job per surviving spec. Each Job emits a
25//! receipt of its own export action so the export itself participates
26//! in the BLAKE3 attestation chain.
27//!
28//! Lisp authoring:
29//! ```lisp
30//! (defephemeral closed-loop-attest
31//!   :aplicacao  (…)
32//!   :ttl        "1h"
33//!   :teardown   OnAttested
34//!   :exports
35//!     (;; Receipts — tier-1 guaranteed delivery via NATS JetStream
36//!      (:source  (:receipts)
37//!       :channel (:nats-subject :subject "pleme.pleme-dev.ephemeral.{{run_id}}.receipt"
38//!                               :stream  "EPHEMERAL_RECEIPTS")
39//!       :when    OnAttested)
40//!      ;; Test report — best-effort via Vector HTTP ingest
41//!      (:source  (:test-report :configmap "demo-test-results"
42//!                              :key       "junit.xml"
43//!                              :format    Junit)
44//!       :channel (:http-event :signal-type "test-report")
45//!       :when    Always)
46//!      ;; Run marker — small synthetic event for shinryu cohort math
47//!      (:source  (:run-marker :labels (:run-id "{{run_id}}"
48//!                                       :phase "end"))
49//!       :channel (:http-event :signal-type "ephemeral-marker")
50//!       :when    Always)))
51//! ```
52
53use schemars::JsonSchema;
54use serde::{Deserialize, Serialize};
55use std::collections::BTreeMap;
56use std::fmt;
57
58use crate::phase::ProcessPhase;
59
60// ─── ExportSpec ────────────────────────────────────────────────────
61
62/// One declared export from an ephemeral Process.
63///
64/// Multiple `ExportSpec`s can be attached to a single ephemeral
65/// lifetime — each fires independently during the `Releasing` phase
66/// when its [`ExportTrigger`] matches the terminal phase reached.
67#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
68#[serde(rename_all = "camelCase")]
69pub struct ExportSpec {
70    /// What artifact to ship.
71    pub source: ArtifactSource,
72
73    /// Where to ship it. Always Vector-native — pleme-io routes every
74    /// emission through one of the four canonical channels.
75    pub channel: VectorChannel,
76
77    /// When to ship. Defaults to `OnAttested`.
78    #[serde(default)]
79    pub when: ExportTrigger,
80
81    /// Override the run-id label that templates into channel subjects
82    /// / signal-type metadata. Defaults to the Process's PID-derived
83    /// run id when unset.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub experiment_id_override: Option<String>,
86}
87
88// ─── ArtifactSource ────────────────────────────────────────────────
89
90/// What artifact this export ships out.
91///
92/// Exactly-one-Option pattern, matching the rest of the typescape
93/// (`Intent`, `Lifetime`). Adding a new artifact kind is additive on
94/// the wire — existing JSON keeps deserializing unchanged.
95#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
96#[serde(rename_all = "camelCase")]
97pub struct ArtifactSource {
98    /// Every `ReceiptEnvelope` emitted by this Process during its
99    /// lifetime — the BLAKE3-chained typed attestation stream.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub receipts: Option<ReceiptsSource>,
102
103    /// A test report stored in a ConfigMap by an in-cluster test
104    /// runner (Job, test harness, closed-loop probe). Worker reads the
105    /// ConfigMap, packages it per `format`, and forwards.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub test_report: Option<TestReportSource>,
108
109    /// The Process's own `ProcessSpec` + `ProcessStatus` snapshot at
110    /// teardown time, as canonical JSON. Useful for post-mortems on
111    /// failed ephemeral runs.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub process_snapshot: Option<ProcessSnapshotSource>,
114
115    /// A small synthetic event — start/end of run markers, cohort
116    /// tags, experiment correlation. Worker emits a single
117    /// timestamped event with the declared `labels`.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub run_marker: Option<RunMarkerSource>,
120}
121
122/// Resolved enum view used by the worker.
123#[derive(Clone, Debug)]
124pub enum ArtifactVariant<'a> {
125    Receipts(&'a ReceiptsSource),
126    TestReport(&'a TestReportSource),
127    ProcessSnapshot(&'a ProcessSnapshotSource),
128    RunMarker(&'a RunMarkerSource),
129}
130
131impl ArtifactVariant<'_> {
132    /// Reverse projection — every borrowed variant knows its
133    /// `ArtifactKind` discriminator. Pairs with `ArtifactKind::select`
134    /// so `ArtifactKind::select(source).map(|v| v.kind())` round-trips
135    /// the closed set on the populated side; pinned by
136    /// `artifact_kind_round_trips_through_variant_kind`. Future
137    /// kind-keyed consumers (metric labels like
138    /// `tatara_exports_total{artifact="receipts"}`, status-condition
139    /// reason strings, audit-trail classifiers, LSP completion) reach
140    /// through this projection instead of pattern-matching the
141    /// payload-carrying view.
142    pub fn kind(&self) -> ArtifactKind {
143        match self {
144            Self::Receipts(_) => ArtifactKind::Receipts,
145            Self::TestReport(_) => ArtifactKind::TestReport,
146            Self::ProcessSnapshot(_) => ArtifactKind::ProcessSnapshot,
147            Self::RunMarker(_) => ArtifactKind::RunMarker,
148        }
149    }
150}
151
152impl crate::tagged_union::VariantKind<ArtifactKind> for ArtifactVariant<'_> {
153    fn variant_kind(&self) -> ArtifactKind {
154        self.kind()
155    }
156}
157
158/// Closed-set discriminator over `ArtifactSource`'s four tagged-union
159/// slots. Single source of truth that drives `ArtifactSource::variant`'s
160/// ambiguity + emptiness resolver, the `ArtifactError::Empty` message,
161/// and the reverse `ArtifactVariant::kind` projection. Adding a fifth
162/// artifact variant lands at one `ALL` entry + one `as_str` arm + one
163/// `select` arm + one `ArtifactVariant::kind` arm — exhaustively
164/// checked by the compiler.
165///
166/// Sibling closed-set lifts on the same `ExportSpec` axis:
167/// [`crate::intent::IntentKind::ALL`], [`crate::lifetime::LifetimeKind::ALL`],
168/// [`ExportTrigger::ALL`], [`ReportFormat::ALL`],
169/// [`crate::lifetime::TeardownPolicy::ALL`].
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
171#[closed_set(via = "as_str", display, generate_unknown)]
172pub enum ArtifactKind {
173    Receipts,
174    TestReport,
175    ProcessSnapshot,
176    RunMarker,
177}
178
179impl ArtifactKind {
180    /// The closed set of artifact kinds — single source of truth that
181    /// drives `ArtifactSource::variant`'s sweep so a variant added
182    /// without an `ALL` entry never reaches the resolver. The `[Self; 4]`
183    /// array literal forces the arity at compile time.
184    pub const ALL: [Self; 4] = [
185        Self::Receipts,
186        Self::TestReport,
187        Self::ProcessSnapshot,
188        Self::RunMarker,
189    ];
190
191    /// Canonical camelCase wire-format key — matches the serde
192    /// `rename_all = "camelCase"` field name on `ArtifactSource`. The
193    /// `ArtifactError::Empty` message composes the human-readable list
194    /// from this projection so a new variant lands in the
195    /// operator-facing diagnostic automatically via the `ALL` sweep,
196    /// not via hand-maintained error-string drift. Pinned by
197    /// `artifact_kind_as_str_matches_field_name`.
198    pub const fn as_str(self) -> &'static str {
199        match self {
200            Self::Receipts => "receipts",
201            Self::TestReport => "testReport",
202            Self::ProcessSnapshot => "processSnapshot",
203            Self::RunMarker => "runMarker",
204        }
205    }
206
207    /// Project an `ArtifactSource` borrow into the optional typed variant
208    /// view for this kind. Returns `None` iff the matching slot is
209    /// `None`. Composes the closed-set sweep `ArtifactSource::variant`
210    /// loops over. Mirrors [`crate::intent::IntentKind::select`].
211    pub fn select<'a>(self, source: &'a ArtifactSource) -> Option<ArtifactVariant<'a>> {
212        match self {
213            Self::Receipts => source.receipts.as_ref().map(ArtifactVariant::Receipts),
214            Self::TestReport => source.test_report.as_ref().map(ArtifactVariant::TestReport),
215            Self::ProcessSnapshot => source
216                .process_snapshot
217                .as_ref()
218                .map(ArtifactVariant::ProcessSnapshot),
219            Self::RunMarker => source.run_marker.as_ref().map(ArtifactVariant::RunMarker),
220        }
221    }
222}
223
224// `impl fmt::Display for ArtifactKind` + `impl FromStr for
225// ArtifactKind` + `impl tatara_lisp::ClosedSet for ArtifactKind` +
226// `pub struct UnknownArtifactKind(pub String)` are generated by
227// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(via =
228// "as_str", display, generate_unknown)]` on the enum declaration above.
229// The auto-derived label `"artifact kind"` matches the prior hand-
230// rolled `#[error("unknown artifact kind: {0}")]` verbatim. The
231// inherent `as_str` projection stays load-bearing — the camelCase
232// wire-format that matches the serde rename + the `ArtifactSource`
233// struct-field name + the `ArtifactError::Empty` diagnostic verbatim
234// — while the trait method `label` gives generic consumers a STABLE
235// name across the workspace-wide closed-set implementors.
236
237crate::declare_tagged_union_error! {
238    pub ArtifactError,
239    empty = "artifact source has no variant set (one of {0} required)",
240    ambiguous = "artifact source has multiple variants set; exactly one required",
241}
242
243/// Slash-joined list of every `ArtifactKind::as_str()` — composed once
244/// at compile time so `ArtifactError::Empty`'s diagnostic carries the
245/// closed-set summary without per-variant string drift. Mirrors
246/// [`crate::intent::INTENT_KIND_LIST`] in shape.
247pub(crate) const ARTIFACT_KIND_LIST: &str = "receipts/testReport/processSnapshot/runMarker";
248
249crate::declare_tagged_union_impls! {
250    parent = ArtifactSource,
251    kind = ArtifactKind,
252    variant = ArtifactVariant,
253    error = ArtifactError,
254    kind_list = ARTIFACT_KIND_LIST,
255}
256
257/// Receipts source — no fields. The worker reads every
258/// `ReceiptEnvelope` annotated with this Process's PID.
259#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
260#[serde(rename_all = "camelCase")]
261pub struct ReceiptsSource {}
262
263/// Test report source — a ConfigMap key with optional format hint.
264#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
265#[serde(rename_all = "camelCase")]
266pub struct TestReportSource {
267    /// ConfigMap name (in the Process's namespace) the runner wrote to.
268    pub configmap: String,
269    /// Key inside the ConfigMap holding the report bytes.
270    pub key: String,
271    /// Report shape hint — downstream parsers in shinryu key off this.
272    #[serde(default)]
273    pub format: ReportFormat,
274    /// Optional ConfigMap namespace override.
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub namespace: Option<String>,
277}
278
279/// Process snapshot source — bundles spec + status as JSON.
280#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
281#[serde(rename_all = "camelCase")]
282pub struct ProcessSnapshotSource {
283    /// When true, also bundle Process attestation history.
284    #[serde(default)]
285    pub include_attestation_chain: bool,
286}
287
288/// Run marker source — small synthetic event with labels.
289#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
290#[serde(rename_all = "camelCase")]
291pub struct RunMarkerSource {
292    /// Labels emitted on the marker event. Free-form key/value;
293    /// downstream consumers (shinryu cohort math, vector transforms)
294    /// read by key name.
295    #[serde(default)]
296    pub labels: BTreeMap<String, String>,
297}
298
299/// Bytes-shape hint for `TestReportSource`. Tatara emits the bytes
300/// untransformed and tags the Vector event with this so shinryu
301/// can route to the right parser tier.
302#[derive(
303    Clone,
304    Copy,
305    Debug,
306    PartialEq,
307    Eq,
308    Hash,
309    Serialize,
310    Deserialize,
311    JsonSchema,
312    Default,
313    tatara_closed_set::DeriveClosedSet,
314)]
315#[serde(rename_all = "PascalCase")]
316#[closed_set(via = "as_str", generate_unknown)]
317pub enum ReportFormat {
318    /// xUnit / JUnit XML — the closed-loop probe + test harnesses emit this.
319    Junit,
320    /// TAP v13 — Bash/Bats test suites.
321    TapV13,
322    /// Newline-delimited JSON — one event per line, native shinryu shape.
323    NdJson,
324    /// Opaque bytes — no parser hint, downstream stores as-is.
325    #[default]
326    Raw,
327}
328
329/// How the export worker should embed a `TestReportSource`'s bytes into
330/// the shipped `ExportEvent.payload`. Lifted as a closed-set typed
331/// projection from [`ReportFormat::payload_shape`] so the worker's
332/// dispatch is exhaustive on `ReportPayloadShape`, not on `ReportFormat`
333/// with a silent `_` arm. Adding a future `ReportFormat` variant forces
334/// the author to pick its shape here (single edit site); adding a
335/// future shape (e.g. compressed) forces every consumer to handle it.
336///
337/// The (shape, JSON-embed-field) pairing — `NdJsonLines` → `"ndjson"`,
338/// `OpaqueBytes` → `"raw_b64"` — binds at ONE typed projection
339/// ([`Self::payload_field`]) rather than at the future worker's
340/// embed-site string literals; pre-lift the field names lived in this
341/// enum's per-variant docstring prose AND would have lived at the
342/// worker's `payload.insert("ndjson", …)` / `payload.insert("raw_b64",
343/// …)` call sites, where a rename of `"raw_b64"` → `"raw"` at one site
344/// drifts silently from the docstring and the operator-facing shinryu
345/// schema.
346///
347/// Sibling typed-projection lift over a closed enum (rather than
348/// `matches!` / `_` arm dispatch):
349/// [`crate::lifetime::TeardownPolicy::should_teardown_on`],
350/// [`ExportTrigger::fires_on`], [`crate::phase::ProcessPhase::as_str`].
351///
352/// Sibling closed-set [`Self::ALL`] lift in lockstep with every other
353/// `ALL`-keyed enum on the same `ExportSpec` axis ([`ReportFormat::ALL`],
354/// [`ExportTrigger::ALL`]) and across the crate
355/// ([`crate::phase::ProcessPhase::ALL`],
356/// [`crate::signal::ProcessSignal::ALL`],
357/// [`crate::boundary::ConditionKind::ALL`],
358/// [`crate::lifetime::TeardownPolicy::ALL`]).
359#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
360#[closed_set(via = "as_str", display, generate_unknown = "report payload shape")]
361pub enum ReportPayloadShape {
362    /// Newline-delimited JSON — split on `\n`, parse each non-empty
363    /// line as JSON, embed under `payload.<payload_field>` (=
364    /// `payload.ndjson`) as an array. Field name is the typed
365    /// projection [`Self::payload_field`] so no embed-site literal
366    /// can drift from this docstring.
367    NdJsonLines,
368    /// Opaque bytes — base64-encode the artifact verbatim, embed
369    /// under `payload.<payload_field>` (= `payload.raw_b64`) as a
370    /// string. The default shape for anything Vector / shinryu
371    /// shouldn't pre-parse. Field name is the typed projection
372    /// [`Self::payload_field`].
373    OpaqueBytes,
374}
375
376impl ReportPayloadShape {
377    /// The closed set of payload shapes — single source of truth
378    /// that drives the [`Self::as_str`] / [`fmt::Display`] pair and
379    /// the [`Self::payload_field`] projection. Adding a third shape
380    /// (e.g. `Compressed` → `"gzip"`) lands at one `ALL` entry + one
381    /// `as_str` arm + one `payload_field` arm + at least one new
382    /// `ReportFormat::payload_shape` mapping — exhaustively checked
383    /// by the compiler (the `[Self; 2]` array literal forces the
384    /// arity).
385    ///
386    /// Sibling closed-set lifts on the same `ExportSpec` axis:
387    /// [`ReportFormat::ALL`], [`ExportTrigger::ALL`],
388    /// [`crate::lifetime::TeardownPolicy::ALL`],
389    /// [`crate::boundary::ConditionKind::ALL`],
390    /// [`crate::phase::ProcessPhase::ALL`],
391    /// [`crate::signal::ProcessSignal::ALL`],
392    /// [`crate::encapsulates::EncapsulationMode::ALL`].
393    pub const ALL: [Self; 2] = [Self::NdJsonLines, Self::OpaqueBytes];
394
395    /// Canonical PascalCase identifier — used by [`fmt::Display`] (so
396    /// `format!("{shape}")` never reaches for `{:?}` Debug) and as
397    /// the operator-facing reason-string projection. No serde wire
398    /// shape today (the enum is worker-internal), but the identifier
399    /// matches the sibling-aligned `as_str` shape that every other
400    /// closed-set enum in this crate exposes
401    /// ([`ReportFormat::as_str`], [`ExportTrigger::as_str`],
402    /// [`crate::phase::ProcessPhase::as_str`]). Pinned by
403    /// `report_payload_shape_as_str_unique_per_variant`.
404    pub const fn as_str(self) -> &'static str {
405        match self {
406            Self::NdJsonLines => "NdJsonLines",
407            Self::OpaqueBytes => "OpaqueBytes",
408        }
409    }
410
411    /// The JSON field name within `ExportEvent.payload` where the
412    /// worker embeds this shape's encoded value — `"ndjson"` for the
413    /// newline-split array, `"raw_b64"` for the base64-encoded
414    /// opaque string. Pre-lift these names lived ONLY in the
415    /// per-variant docstring prose; once the export worker lands, a
416    /// rename at the embed site (`payload.insert("ndjson", …)` →
417    /// `payload.insert("events", …)`) silently drifts from the
418    /// docstring and from the operator-facing shinryu schema with no
419    /// compile or runtime signal. Post-lift the worker's embed site
420    /// is `payload.insert(shape.payload_field().into(), …)` and a
421    /// rename lands at ONE arm here. The per-variant uniqueness
422    /// invariant (no two shapes alias to the same field name) is
423    /// pinned by `report_payload_shape_payload_field_unique_per_
424    /// variant` so the worker's embed site cannot have two shapes
425    /// collide on the same destination key. Truth table pinned by
426    /// `report_payload_shape_payload_field_truth_table` so a future
427    /// rename lands here at one site, not in the worker.
428    pub const fn payload_field(self) -> &'static str {
429        match self {
430            Self::NdJsonLines => "ndjson",
431            Self::OpaqueBytes => "raw_b64",
432        }
433    }
434}
435
436// `impl fmt::Display for ReportPayloadShape` +
437// `impl std::str::FromStr for ReportPayloadShape` +
438// `impl tatara_lisp::ClosedSet for ReportPayloadShape` +
439// `pub struct UnknownReportPayloadShape(pub String)` are all generated
440// by `#[derive(tatara_closed_set::DeriveClosedSet)]` +
441// `#[closed_set(via = "as_str", display, generate_unknown = "report
442// payload shape")]` on the enum declaration above. The explicit label
443// preserves the natural spelling "report payload shape" against the
444// auto-projection `pascal_to_spaced_lowercase("ReportPayloadShape")`.
445// The inherent `as_str` projection stays load-bearing — the canonical
446// `"NdJsonLines" | "OpaqueBytes"` string every worker-facing reason
447// projection reads; `via = "as_str"` binds `ClosedSet::label` to the
448// same projection so the substrate-wide
449// `assert_display_matches_label` / `assert_closed_set_well_formed`
450// primitives dispatch through the same byte-identical shape every other
451// closed-set implementor across the crate publishes. Aligns
452// `ReportPayloadShape` with the substrate-wide
453// `#[derive(DeriveClosedSet)]` idiom that every sibling closed-set enum
454// on this `ExportSpec` axis (`ReportFormat`, `ArtifactKind`,
455// `ChannelKind`, `ExportTrigger`) already carries — the last hand-rolled
456// `impl fmt::Display` on the axis is closed at ONE substrate site.
457
458impl ReportFormat {
459    /// The closed set of report formats — single source of truth that
460    /// drives the `as_str` / Display / `FromStr` triad and the typed
461    /// `payload_shape` dispatch. Adding a fifth variant lands at one
462    /// `ALL` entry + one `as_str` arm + one `payload_shape` arm —
463    /// exhaustively checked by the compiler (the `[Self; 4]` array
464    /// literal forces the arity).
465    ///
466    /// Sibling closed-set lifts on the same `ExportSpec` axis:
467    /// [`ExportTrigger::ALL`], [`crate::lifetime::TeardownPolicy::ALL`],
468    /// [`crate::boundary::ConditionKind::ALL`],
469    /// [`crate::phase::ProcessPhase::ALL`],
470    /// [`crate::signal::ProcessSignal::ALL`],
471    /// [`crate::encapsulates::EncapsulationMode::ALL`].
472    pub const ALL: [Self; 4] = [Self::Junit, Self::TapV13, Self::NdJson, Self::Raw];
473
474    /// Canonical PascalCase wire-format projection — matches the serde
475    /// `rename_all = "PascalCase"` output verbatim. Used by Display
476    /// (single source of truth) and by `FromStr`'s sweep of `ALL` so
477    /// the typed surface and the YAML wire format cannot drift. Pinned
478    /// by `report_format_as_str_matches_serde`.
479    pub const fn as_str(self) -> &'static str {
480        match self {
481            Self::Junit => "Junit",
482            Self::TapV13 => "TapV13",
483            Self::NdJson => "NdJson",
484            Self::Raw => "Raw",
485        }
486    }
487
488    /// Typed projection: which payload-embedding strategy the export
489    /// worker should pick for this format. ONE typed dispatch that
490    /// replaces the worker's `match tr.format { NdJson => …, _ => … }`
491    /// silent-default arm. Adding a new `ReportFormat` variant forces
492    /// the author to decide its shape here (the compiler exhaustively
493    /// checks this match); the worker's dispatch on the returned
494    /// `ReportPayloadShape` then remains a closed 2-arm match. Pinned
495    /// by `report_format_payload_shape_truth_table`.
496    pub const fn payload_shape(self) -> ReportPayloadShape {
497        match self {
498            Self::NdJson => ReportPayloadShape::NdJsonLines,
499            Self::Junit | Self::TapV13 | Self::Raw => ReportPayloadShape::OpaqueBytes,
500        }
501    }
502}
503
504impl fmt::Display for ReportFormat {
505    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506        f.write_str(self.as_str())
507    }
508}
509
510// `impl FromStr for ReportFormat` + `impl tatara_lisp::ClosedSet for
511// ReportFormat` are generated by `#[derive(tatara_closed_set::DeriveClosedSet)]`
512// on the enum declaration above — the trait-impl plumbing collapses onto
513// ONE derive line + the `#[closed_set(via = "as_str")]` attribute that
514// names the inherent projection method. Per-implementor content stays
515// at `pub const ALL` + `pub const fn as_str` + the `UnknownReportFormat`
516// carrier below.
517
518// `pub struct UnknownReportFormat(pub String)` is generated by
519// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
520// on the enum declaration above. The auto-derived label `"report format"`
521// matches the prior hand-rolled `#[error("unknown report format: {0}")]`
522// verbatim. Symmetric to [`UnknownChannelKind`],
523// [`UnknownExportTrigger`], [`crate::lifetime::UnknownTeardownPolicy`],
524// [`crate::boundary::UnknownConditionKind`],
525// [`crate::phase::UnknownPhase`].
526
527// ─── VectorChannel ─────────────────────────────────────────────────
528
529/// Where the export bytes flow.
530///
531/// All variants land in the pleme-io Vector + NATS layer — there is
532/// no escape hatch for ad-hoc sinks. Vector's downstream sink graph
533/// (file / Loki / VictoriaLogs / VictoriaMetrics) handles durability
534/// + analytics. This primitive only names the *ingestion* shape.
535#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
536#[serde(rename_all = "camelCase")]
537pub struct VectorChannel {
538    /// HTTP POST to Vector's `http_server` source.
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    pub http_event: Option<HttpEventChannel>,
541
542    /// Publish to a NATS JetStream subject. Use for tier-1
543    /// guaranteed-delivery events (receipts that MUST survive).
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    pub nats_subject: Option<NatsSubjectChannel>,
546
547    /// Print to the export worker's stdout. Vector's
548    /// `kubernetes_logs` source picks it up. Lowest-effort channel;
549    /// fine for debug / one-off exports.
550    #[serde(default, skip_serializing_if = "Option::is_none")]
551    pub stdout: Option<StdoutChannel>,
552}
553
554#[derive(Clone, Debug)]
555pub enum ChannelVariant<'a> {
556    HttpEvent(&'a HttpEventChannel),
557    NatsSubject(&'a NatsSubjectChannel),
558    Stdout(&'a StdoutChannel),
559}
560
561impl ChannelVariant<'_> {
562    /// Reverse projection — every borrowed channel variant knows its
563    /// `ChannelKind` discriminator. Pairs with [`ChannelKind::select`]
564    /// so `ChannelKind::select(channel).map(|v| v.kind())` round-trips
565    /// the closed set on the populated side; pinned by
566    /// `channel_kind_round_trips_through_variant_kind`. Future
567    /// kind-keyed consumers (metric labels like
568    /// `tatara_exports_total{channel="natsSubject"}`, status-condition
569    /// reason strings, audit-trail classifiers, LSP completion) reach
570    /// through this projection instead of pattern-matching the
571    /// payload-carrying view. Mirrors
572    /// [`ArtifactVariant::kind`] and [`crate::intent::IntentVariant::kind`].
573    pub fn kind(&self) -> ChannelKind {
574        match self {
575            Self::HttpEvent(_) => ChannelKind::HttpEvent,
576            Self::NatsSubject(_) => ChannelKind::NatsSubject,
577            Self::Stdout(_) => ChannelKind::Stdout,
578        }
579    }
580}
581
582impl crate::tagged_union::VariantKind<ChannelKind> for ChannelVariant<'_> {
583    fn variant_kind(&self) -> ChannelKind {
584        self.kind()
585    }
586}
587
588/// Closed-set discriminator over `VectorChannel`'s three tagged-union
589/// slots. Single source of truth that drives `VectorChannel::variant`'s
590/// ambiguity + emptiness resolver, the `ChannelError::Empty` message,
591/// and the reverse `ChannelVariant::kind` projection. Adding a fourth
592/// channel variant lands at one `ALL` entry + one `as_str` arm + one
593/// `select` arm + one `ChannelVariant::kind` arm — exhaustively
594/// checked by the compiler.
595///
596/// Sibling closed-set lifts on the same `ExportSpec` axis:
597/// [`ArtifactKind::ALL`], [`ExportTrigger::ALL`], [`ReportFormat::ALL`],
598/// [`ReportPayloadShape::ALL`].
599#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
600#[closed_set(via = "as_str", generate_unknown)]
601pub enum ChannelKind {
602    HttpEvent,
603    NatsSubject,
604    Stdout,
605}
606
607impl ChannelKind {
608    /// The closed set of channel kinds — single source of truth that
609    /// drives `VectorChannel::variant`'s sweep so a variant added
610    /// without an `ALL` entry never reaches the resolver. The
611    /// `[Self; 3]` array literal forces the arity at compile time.
612    pub const ALL: [Self; 3] = [Self::HttpEvent, Self::NatsSubject, Self::Stdout];
613
614    /// Canonical camelCase wire-format key — matches the serde
615    /// `rename_all = "camelCase"` field name on `VectorChannel`. The
616    /// `ChannelError::Empty` message composes the human-readable list
617    /// from this projection so a new variant lands in the
618    /// operator-facing diagnostic automatically via the `ALL` sweep,
619    /// not via hand-maintained error-string drift. Pinned by
620    /// `channel_kind_as_str_matches_field_name`.
621    pub const fn as_str(self) -> &'static str {
622        match self {
623            Self::HttpEvent => "httpEvent",
624            Self::NatsSubject => "natsSubject",
625            Self::Stdout => "stdout",
626        }
627    }
628
629    /// Project a `VectorChannel` borrow into the optional typed variant
630    /// view for this kind. Returns `None` iff the matching slot is
631    /// `None`. Composes the closed-set sweep `VectorChannel::variant`
632    /// loops over. Mirrors [`ArtifactKind::select`] +
633    /// [`crate::intent::IntentKind::select`] +
634    /// [`crate::lifetime::LifetimeKind::select`].
635    pub fn select<'a>(self, channel: &'a VectorChannel) -> Option<ChannelVariant<'a>> {
636        match self {
637            Self::HttpEvent => channel.http_event.as_ref().map(ChannelVariant::HttpEvent),
638            Self::NatsSubject => channel
639                .nats_subject
640                .as_ref()
641                .map(ChannelVariant::NatsSubject),
642            Self::Stdout => channel.stdout.as_ref().map(ChannelVariant::Stdout),
643        }
644    }
645}
646
647impl fmt::Display for ChannelKind {
648    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
649        f.write_str(self.as_str())
650    }
651}
652
653// `impl FromStr for ChannelKind` + `impl tatara_lisp::ClosedSet for
654// ChannelKind` are generated by `#[derive(tatara_closed_set::DeriveClosedSet)]`
655// on the enum declaration above.
656
657// `pub struct UnknownChannelKind(pub String)` is generated by
658// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
659// on the enum declaration above. The auto-derived label `"channel kind"`
660// matches the prior hand-rolled `#[error("unknown channel kind: {0}")]`
661// verbatim. Symmetric to [`UnknownArtifactKind`],
662// [`UnknownReportFormat`], [`UnknownExportTrigger`],
663// [`crate::lifetime::UnknownTeardownPolicy`],
664// [`crate::boundary::UnknownConditionKind`],
665// [`crate::phase::UnknownPhase`].
666
667crate::declare_tagged_union_error! {
668    pub ChannelError,
669    empty = "vector channel has no variant set (one of {0} required)",
670    ambiguous = "vector channel has multiple variants set; exactly one required",
671}
672
673/// Slash-joined list of every `ChannelKind::as_str()` — composed once
674/// at compile time so `ChannelError::Empty`'s diagnostic carries the
675/// closed-set summary without per-variant string drift. Mirrors
676/// [`ARTIFACT_KIND_LIST`] in shape.
677pub(crate) const CHANNEL_KIND_LIST: &str = "httpEvent/natsSubject/stdout";
678
679crate::declare_tagged_union_impls! {
680    parent = VectorChannel,
681    kind = ChannelKind,
682    variant = ChannelVariant,
683    error = ChannelError,
684    kind_list = CHANNEL_KIND_LIST,
685}
686
687/// HTTP POST channel — Vector `http_server` source.
688#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
689#[serde(rename_all = "camelCase")]
690pub struct HttpEventChannel {
691    /// Vector ingest endpoint. Defaults to the in-cluster Service
692    /// `http://vector.observability.svc.cluster.local:8080` when
693    /// unset.
694    #[serde(default, skip_serializing_if = "Option::is_none")]
695    pub endpoint: Option<String>,
696
697    /// `signal_type` tag added to every emitted event. Vector
698    /// transforms + shinryu's analytical schema route by this tag
699    /// (`receipt`, `test-report`, `ephemeral-marker`, …).
700    pub signal_type: String,
701}
702
703/// Default Vector ingest endpoint when `HttpEventChannel.endpoint`
704/// is unset. Single source of truth for downstream tooling.
705pub const DEFAULT_VECTOR_INGEST: &str = "http://vector.observability.svc.cluster.local:8080";
706
707impl HttpEventChannel {
708    /// Resolve the endpoint URL, falling back to the in-cluster default.
709    pub fn resolved_endpoint(&self) -> &str {
710        self.endpoint.as_deref().unwrap_or(DEFAULT_VECTOR_INGEST)
711    }
712
713    /// Compose an [`HttpEventChannel`] pinned to the in-cluster
714    /// default endpoint ([`DEFAULT_VECTOR_INGEST`], stamped as
715    /// `endpoint: None` so [`Self::resolved_endpoint`] resolves the
716    /// same URL on read) and the given `signal_type` — the ONE
717    /// substrate primitive owning the 4-token `HttpEventChannel {
718    /// endpoint: None, signal_type: <s>.into() }` fixture literal every
719    /// consumer restated by hand pre-lift.
720    ///
721    /// Pre-lift the same 3-slot chain (`endpoint: None`,
722    /// `signal_type: <label>.into()`, `..` for the two-slot struct's
723    /// non-existent tail) was hand-authored at TEN workspace-wide
724    /// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold,
725    /// EVERY one of them the "default-endpoint, name-only" shape (no
726    /// site in the workspace stamps a non-None endpoint on an
727    /// `HttpEventChannel` literal):
728    ///
729    /// * `tatara-process::export` — five sites: two closed-set
730    ///   ambiguity probes (`single_slot_channel`, the
731    ///   `kind_equals_variant_kind_across_all_carriers` probe), plus
732    ///   three round-trip tests (`vector_channel_resolves_http_event`,
733    ///   `export_spec_serde_round_trip`, `run_marker_labels_round_trip`).
734    /// * `tatara-process::lifetime` — two round-trip tests
735    ///   (`applicable_exports_filters_by_trigger`,
736    ///   `exports_round_trip_through_lifetime`).
737    /// * `tatara-process::tagged_union` — two closed-set property probes
738    ///   (`single_slot_vector_channel_probe`,
739    ///   `kind_equals_variant_kind_across_all_carriers`).
740    /// * `tatara-reconciler::render` — the `spec_run_marker_always`
741    ///   fixture the ephemeral-export-Job renderer's tests seed.
742    /// * `tatara-export-worker::lib` — the `http_spec` fixture the
743    ///   worker's `resolve_run_id` tests seed.
744    ///
745    /// Post-lift every callsite reads `HttpEventChannel::signal(<label>)`
746    /// and the two-slot struct's `endpoint` slot stays owned by the ONE
747    /// substrate site. The `impl Into<String>` bound accepts every
748    /// pre-lift caller shape verbatim — `&'static str` literals
749    /// (`"receipt"`, `"test-report"`, `"ephemeral-marker"`), owned
750    /// `String` values (worker fixture's `signal_type.to_string()`), and
751    /// `.into()`-terminated chains alike — without a per-site coercion.
752    ///
753    /// A future addition (a default-endpoint override for a
754    /// per-fleet Vector ingress, a `signal_type` normalization step
755    /// clamping the tag to shinryu's allowed set, a per-fleet
756    /// `endpoint` seed pulled from a config surface) lands at THIS
757    /// ONE substrate primitive and every downstream consumer inherits
758    /// the upgrade mechanically — no per-site edit at any of the ten
759    /// listed callers or at future test fixtures.
760    ///
761    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
762    /// the `HttpEventChannel { endpoint: None, signal_type: <s>.into() }`
763    /// fixture literal recurred at ten hand-authored sites past the
764    /// ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
765    /// ONE owner here). THEORY.md §II.1 invariant 5 (composition
766    /// preserves proofs — a regression that drifted the default-
767    /// endpoint sentinel from `None` to a hardcoded string, or
768    /// reordered the two struct slots, surfaces at the
769    /// `signal_composes_byte_identical_to_pre_lift_literal_across_every_label`
770    /// pin below rather than as silent skew at every downstream fixture).
771    #[must_use]
772    pub fn signal(signal_type: impl Into<String>) -> Self {
773        Self {
774            endpoint: None,
775            signal_type: signal_type.into(),
776        }
777    }
778}
779
780/// NATS JetStream channel — guaranteed-delivery publish.
781#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
782#[serde(rename_all = "camelCase")]
783pub struct NatsSubjectChannel {
784    /// Subject to publish to. May contain `{{run_id}}` template
785    /// substitution — the worker substitutes the resolved run id at
786    /// publish time.
787    pub subject: String,
788
789    /// JetStream stream the subject belongs to. The stream itself is
790    /// declared by the consumer chart (e.g. tatara-pool-reconciler)
791    /// via the pleme-nats broker-only design.
792    pub stream: String,
793
794    /// Optional NATS URL. Defaults to `nats://nats.observability.svc.cluster.local:4222`.
795    #[serde(default, skip_serializing_if = "Option::is_none")]
796    pub url: Option<String>,
797}
798
799/// Default NATS URL when `NatsSubjectChannel.url` is unset.
800pub const DEFAULT_NATS_URL: &str = "nats://nats.observability.svc.cluster.local:4222";
801
802impl NatsSubjectChannel {
803    /// Resolve the NATS URL, falling back to the in-cluster default.
804    pub fn resolved_url(&self) -> &str {
805        self.url.as_deref().unwrap_or(DEFAULT_NATS_URL)
806    }
807}
808
809/// Stdout channel — worker prints the event; Vector picks up via
810/// `kubernetes_logs`.
811#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
812#[serde(rename_all = "camelCase")]
813pub struct StdoutChannel {
814    /// Pretty-print JSON (multi-line) instead of compact NDJSON.
815    /// Defaults to false — compact NDJSON matches Vector's parser.
816    #[serde(default)]
817    pub pretty: bool,
818}
819
820// ─── ExportTrigger ─────────────────────────────────────────────────
821
822/// When the export fires. Aligns with `ProcessPhase` so the
823/// reconciler's `Releasing` phase can match against the terminal
824/// phase reached directly.
825#[derive(
826    Clone,
827    Copy,
828    Debug,
829    PartialEq,
830    Eq,
831    Hash,
832    Serialize,
833    Deserialize,
834    JsonSchema,
835    Default,
836    tatara_closed_set::DeriveClosedSet,
837)]
838#[serde(rename_all = "PascalCase")]
839#[closed_set(via = "as_str", generate_unknown)]
840pub enum ExportTrigger {
841    /// Fire when the Process reaches `Attested`. Default — matches
842    /// the most common case (capture successful-run artifacts).
843    #[default]
844    OnAttested,
845    /// Fire when the Process reaches `Failed`. Use for failure
846    /// post-mortems (process snapshots, last receipts).
847    OnFailed,
848    /// Fire on every terminal phase (`Attested` or `Failed`). Use
849    /// for run markers that need to surface regardless of outcome.
850    Always,
851}
852
853impl ExportTrigger {
854    /// The closed set of export triggers — single source of truth that
855    /// drives the `as_str` / Display / `FromStr` triad and the typed
856    /// `fires_on` dispatch over `ProcessPhase`. Adding a fourth variant
857    /// lands at one `ALL` entry + one `as_str` arm + one `fires_on` arm
858    /// — exhaustively checked by the compiler (the `[Self; 3]` array
859    /// literal forces the arity).
860    ///
861    /// Sibling closed-set lifts on the same `ProcessSpec` axis:
862    /// [`crate::lifetime::TeardownPolicy::ALL`],
863    /// [`crate::intent::IntentKind::ALL`],
864    /// [`crate::lifetime::LifetimeKind::ALL`],
865    /// [`crate::boundary::ConditionKind::ALL`],
866    /// [`crate::phase::ProcessPhase::ALL`],
867    /// [`crate::signal::ProcessSignal::ALL`].
868    pub const ALL: [Self; 3] = [Self::OnAttested, Self::OnFailed, Self::Always];
869
870    /// Canonical PascalCase wire-format projection — matches the serde
871    /// `rename_all = "PascalCase"` output verbatim. Used by Display
872    /// (single source of truth), by `FromStr` to identify the variant
873    /// from its annotation / status-field representation, and by
874    /// operator-facing reason strings without reaching for `{:?}` Debug
875    /// formatting. Pinned by `export_trigger_as_str_matches_serde`.
876    pub const fn as_str(self) -> &'static str {
877        match self {
878            Self::OnAttested => "OnAttested",
879            Self::OnFailed => "OnFailed",
880            Self::Always => "Always",
881        }
882    }
883
884    /// True iff, given a `ProcessPhase`, this trigger says "fire."
885    /// ONE typed dispatch over the typed phase enum that replaces the
886    /// four hand-rolled `match phase { Attested => fires_on_attested(),
887    /// Failed => fires_on_failed(), _ => false }` sites the reconciler
888    /// and `EphemeralLifetime` previously branched on. Every
889    /// non-terminal phase always returns `false` — exports are a
890    /// terminal-phase decision, now enforced by the closed-set match
891    /// over `ProcessPhase`.
892    ///
893    /// The legacy [`Self::fires_on_attested`] / [`Self::fires_on_failed`]
894    /// predicates remain as thin delegates so existing call sites keep
895    /// their narrow signatures; the truth table is pinned by
896    /// `export_trigger_legacy_predicates_delegate_to_phase_dispatch`.
897    pub const fn fires_on(self, phase: ProcessPhase) -> bool {
898        match phase {
899            ProcessPhase::Attested => matches!(self, Self::OnAttested | Self::Always),
900            ProcessPhase::Failed => matches!(self, Self::OnFailed | Self::Always),
901            ProcessPhase::Pending
902            | ProcessPhase::Forking
903            | ProcessPhase::Execing
904            | ProcessPhase::Running
905            | ProcessPhase::Reconverging
906            | ProcessPhase::Releasing
907            | ProcessPhase::Exiting
908            | ProcessPhase::Zombie
909            | ProcessPhase::Reaped => false,
910        }
911    }
912
913    /// Thin delegate to [`Self::fires_on`] for the `Attested` case —
914    /// kept so existing call sites that already know the gate keep
915    /// their narrow signature without reaching for the typed-phase
916    /// variant.
917    pub const fn fires_on_attested(self) -> bool {
918        self.fires_on(ProcessPhase::Attested)
919    }
920
921    /// Symmetric delegate to [`Self::fires_on`] for the `Failed` case.
922    pub const fn fires_on_failed(self) -> bool {
923        self.fires_on(ProcessPhase::Failed)
924    }
925}
926
927impl fmt::Display for ExportTrigger {
928    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
929        f.write_str(self.as_str())
930    }
931}
932
933// `impl FromStr for ExportTrigger` + `impl tatara_lisp::ClosedSet for
934// ExportTrigger` are generated by `#[derive(tatara_closed_set::DeriveClosedSet)]`
935// on the enum declaration above.
936
937// `pub struct UnknownExportTrigger(pub String)` is generated by
938// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
939// on the enum declaration above. The auto-derived label `"export trigger"`
940// matches the prior hand-rolled `#[error("unknown export trigger: {0}")]`
941// verbatim. Symmetric to [`UnknownChannelKind`],
942// [`UnknownReportFormat`], [`crate::lifetime::UnknownTeardownPolicy`],
943// [`crate::boundary::UnknownConditionKind`], and
944// [`crate::phase::UnknownPhase`].
945
946// ─── Tests ─────────────────────────────────────────────────────────
947
948#[cfg(test)]
949mod tests {
950    use super::*;
951
952    #[test]
953    fn artifact_source_empty_errors() {
954        let s = ArtifactSource::default();
955        match s.variant().unwrap_err() {
956            ArtifactError::Empty(list) => assert_eq!(list, ARTIFACT_KIND_LIST),
957            other => panic!("expected Empty, got {other:?}"),
958        }
959    }
960
961    #[test]
962    fn artifact_source_receipts_resolves() {
963        let s = ArtifactSource {
964            receipts: Some(ReceiptsSource::default()),
965            ..ArtifactSource::default()
966        };
967        assert!(matches!(s.variant().unwrap(), ArtifactVariant::Receipts(_)));
968    }
969
970    #[test]
971    fn artifact_source_two_variants_ambiguous() {
972        let s = ArtifactSource {
973            receipts: Some(ReceiptsSource::default()),
974            test_report: Some(TestReportSource {
975                configmap: "x".into(),
976                key: "y".into(),
977                format: ReportFormat::Junit,
978                namespace: None,
979            }),
980            ..ArtifactSource::default()
981        };
982        assert_eq!(s.variant().unwrap_err(), ArtifactError::Ambiguous);
983    }
984
985    #[test]
986    fn vector_channel_empty_errors() {
987        let c = VectorChannel::default();
988        match c.variant().unwrap_err() {
989            ChannelError::Empty(list) => assert_eq!(list, CHANNEL_KIND_LIST),
990            other => panic!("expected Empty, got {other:?}"),
991        }
992    }
993
994    #[test]
995    fn vector_channel_resolves_http_event() {
996        let c = VectorChannel {
997            http_event: Some(HttpEventChannel::signal("test-report")),
998            ..VectorChannel::default()
999        };
1000        match c.variant().unwrap() {
1001            ChannelVariant::HttpEvent(h) => {
1002                assert_eq!(h.signal_type, "test-report");
1003                assert_eq!(h.resolved_endpoint(), DEFAULT_VECTOR_INGEST);
1004            }
1005            other => panic!("expected HttpEvent, got {other:?}"),
1006        }
1007    }
1008
1009    #[test]
1010    fn vector_channel_resolves_nats_subject() {
1011        let c = VectorChannel {
1012            nats_subject: Some(NatsSubjectChannel {
1013                subject: "pleme.pleme-dev.ephemeral.{{run_id}}.receipt".into(),
1014                stream: "EPHEMERAL_RECEIPTS".into(),
1015                url: None,
1016            }),
1017            ..VectorChannel::default()
1018        };
1019        match c.variant().unwrap() {
1020            ChannelVariant::NatsSubject(n) => {
1021                assert_eq!(n.stream, "EPHEMERAL_RECEIPTS");
1022                assert_eq!(n.resolved_url(), DEFAULT_NATS_URL);
1023            }
1024            other => panic!("expected NatsSubject, got {other:?}"),
1025        }
1026    }
1027
1028    #[test]
1029    fn export_trigger_fire_logic() {
1030        assert!(ExportTrigger::OnAttested.fires_on_attested());
1031        assert!(!ExportTrigger::OnAttested.fires_on_failed());
1032        assert!(ExportTrigger::OnFailed.fires_on_failed());
1033        assert!(!ExportTrigger::OnFailed.fires_on_attested());
1034        assert!(ExportTrigger::Always.fires_on_attested());
1035        assert!(ExportTrigger::Always.fires_on_failed());
1036    }
1037
1038    // ── closed-set algebra for ExportTrigger (ALL × as_str × FromStr ×
1039    //    fires_on(phase)) ─
1040
1041    /// `ALL` is the source of truth for the resolver / `FromStr` sweep
1042    /// Structural well-formedness of [`ExportTrigger`] as a
1043    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1044    /// testkit lift that pins all three structural invariants (`ALL`
1045    /// is non-empty, every variant round-trips through
1046    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1047    /// outside the closed set) at ONE call site. Replaces the hand-
1048    /// derived `export_trigger_all_is_unique_and_complete` +
1049    /// `export_trigger_roundtrip_via_as_str` + the empty-input arm of
1050    /// `unknown_export_trigger_errors`. `FromStr` delegates to
1051    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1052    /// exercises the same code path the reconciler hits when parsing
1053    /// a CRD `enum:`-validated value back to the typed trigger.
1054    #[test]
1055    fn export_trigger_is_well_formed_closed_set() {
1056        tatara_closed_set::assert_closed_set_well_formed::<ExportTrigger>();
1057    }
1058
1059    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1060    /// output verbatim for every variant. A future variant rename
1061    /// (or an `as_str` arm typo) lands here at one site, instead of
1062    /// drifting between the typed surface and the YAML wire format
1063    /// the reconciler / operator both read.
1064    #[test]
1065    fn export_trigger_as_str_matches_serde() {
1066        crate::tagged_union::assert_label_matches_serde_serialization::<ExportTrigger>();
1067    }
1068
1069    /// The Display impl IS `as_str` — pinning this lets future callers
1070    /// reach for either projection without drift. If a reviewer
1071    /// accidentally re-introduces an inline match in Display, this
1072    /// test would fail the moment a variant rename touches one site
1073    /// but not the other.
1074    #[test]
1075    fn export_trigger_display_matches_as_str() {
1076        crate::tagged_union::assert_display_matches_label::<ExportTrigger>();
1077    }
1078
1079    /// `FromStr` rejects strings that aren't in the canonical
1080    /// projection — lowercased / typo / unrelated — and the error
1081    /// echoes the input verbatim so the operator-facing diagnostic
1082    /// carries the offending value, not a normalized form. The
1083    /// empty-input arm is pinned by
1084    /// [`export_trigger_is_well_formed_closed_set`] via the
1085    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1086    /// verbatim-echo contract on the [`UnknownExportTrigger`] newtype,
1087    /// which the trait's `make_unknown` can't see.
1088    #[test]
1089    fn unknown_export_trigger_errors() {
1090        use std::str::FromStr;
1091        for bad in ["onAttested", "ALWAYS", "Never", "OnSuccess"] {
1092            let err = ExportTrigger::from_str(bad).unwrap_err();
1093            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1094        }
1095    }
1096
1097    // `unknown_export_trigger_message_matches_substrate_convention`
1098    // removed — clause (5) of
1099    // `tatara_closed_set::assert_closed_set_well_formed::<ExportTrigger>()`
1100    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
1101    // shape generically (called from `trigger_is_well_formed_closed_set`
1102    // above); the `SET_LABEL` projection is pinned by
1103    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
1104
1105    /// TRUTH-TABLE CONTRACT: `fires_on(phase)` agrees with the
1106    /// documented (trigger, phase) -> bool table for every (3 × 11)
1107    /// combination. A new variant in either `ExportTrigger` or
1108    /// `ProcessPhase` reaches this test by iteration — adding a phase
1109    /// without extending `fires_on`'s match would be caught by the
1110    /// compiler (the closed-set match over `ProcessPhase` enforces it);
1111    /// adding a trigger without extending its truth row is caught
1112    /// here.
1113    #[test]
1114    fn export_trigger_fires_on_truth_table() {
1115        // ProcessPhase imports are local to the test to keep the
1116        // module's top-level surface minimal.
1117        use crate::phase::ProcessPhase::{
1118            Attested, Execing, Exiting, Failed, Forking, Pending, Reaped, Reconverging, Releasing,
1119            Running, Zombie,
1120        };
1121        let table: &[(ExportTrigger, &[(crate::phase::ProcessPhase, bool)])] = &[
1122            (
1123                ExportTrigger::OnAttested,
1124                &[
1125                    (Attested, true),
1126                    (Failed, false),
1127                    (Pending, false),
1128                    (Forking, false),
1129                    (Execing, false),
1130                    (Running, false),
1131                    (Reconverging, false),
1132                    (Releasing, false),
1133                    (Exiting, false),
1134                    (Zombie, false),
1135                    (Reaped, false),
1136                ],
1137            ),
1138            (
1139                ExportTrigger::OnFailed,
1140                &[
1141                    (Attested, false),
1142                    (Failed, true),
1143                    (Pending, false),
1144                    (Forking, false),
1145                    (Execing, false),
1146                    (Running, false),
1147                    (Reconverging, false),
1148                    (Releasing, false),
1149                    (Exiting, false),
1150                    (Zombie, false),
1151                    (Reaped, false),
1152                ],
1153            ),
1154            (
1155                ExportTrigger::Always,
1156                &[
1157                    (Attested, true),
1158                    (Failed, true),
1159                    (Pending, false),
1160                    (Forking, false),
1161                    (Execing, false),
1162                    (Running, false),
1163                    (Reconverging, false),
1164                    (Releasing, false),
1165                    (Exiting, false),
1166                    (Zombie, false),
1167                    (Reaped, false),
1168                ],
1169            ),
1170        ];
1171        // The truth table must cover every (trigger, phase) pair.
1172        assert_eq!(table.len(), ExportTrigger::ALL.len());
1173        for (_, row) in table {
1174            assert_eq!(row.len(), crate::phase::ProcessPhase::ALL.len());
1175        }
1176        for (trigger, row) in table {
1177            for (phase, expected) in *row {
1178                assert_eq!(
1179                    trigger.fires_on(*phase),
1180                    *expected,
1181                    "fires_on({trigger:?}, {phase:?}) drift"
1182                );
1183            }
1184        }
1185    }
1186
1187    /// DELEGATION CONTRACT: the legacy `fires_on_attested` /
1188    /// `fires_on_failed` predicates agree with the typed
1189    /// `fires_on(phase)` dispatch they delegate to, for every variant
1190    /// in `ALL`. A regression that re-introduces an inline `matches!`
1191    /// in either legacy predicate fails here. `fires_on` is the
1192    /// source of truth.
1193    #[test]
1194    fn export_trigger_legacy_predicates_delegate_to_phase_dispatch() {
1195        for trigger in ExportTrigger::ALL {
1196            assert_eq!(
1197                trigger.fires_on_attested(),
1198                trigger.fires_on(crate::phase::ProcessPhase::Attested),
1199                "legacy fires_on_attested drift for {trigger:?}"
1200            );
1201            assert_eq!(
1202                trigger.fires_on_failed(),
1203                trigger.fires_on(crate::phase::ProcessPhase::Failed),
1204                "legacy fires_on_failed drift for {trigger:?}"
1205            );
1206        }
1207    }
1208
1209    // ── closed-set algebra for ReportFormat (ALL × as_str × FromStr ×
1210    //    payload_shape) ─
1211
1212    /// Structural well-formedness of [`ReportFormat`] as a
1213    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1214    /// testkit lift that pins all three structural invariants (`ALL`
1215    /// is non-empty, every variant round-trips through
1216    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1217    /// outside the closed set) at ONE call site. Replaces the hand-
1218    /// derived `report_format_all_is_unique_and_complete` +
1219    /// `report_format_roundtrip_via_as_str` + the empty-input arm of
1220    /// `unknown_report_format_errors`. `FromStr` delegates to
1221    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1222    /// exercises the same code path the export worker hits when
1223    /// parsing a CRD `enum:`-validated value back to the typed format.
1224    #[test]
1225    fn report_format_is_well_formed_closed_set() {
1226        tatara_closed_set::assert_closed_set_well_formed::<ReportFormat>();
1227    }
1228
1229    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1230    /// output verbatim for every variant. A future variant rename
1231    /// (or an `as_str` arm typo) lands here at one site, instead of
1232    /// drifting between the typed surface and the YAML wire format
1233    /// the reconciler / operator both read.
1234    #[test]
1235    fn report_format_as_str_matches_serde() {
1236        crate::tagged_union::assert_label_matches_serde_serialization::<ReportFormat>();
1237    }
1238
1239    /// The Display impl IS `as_str` — pinning this lets future callers
1240    /// reach for either projection without drift.
1241    #[test]
1242    fn report_format_display_matches_as_str() {
1243        crate::tagged_union::assert_display_matches_label::<ReportFormat>();
1244    }
1245
1246    /// `FromStr` rejects strings that aren't in the canonical
1247    /// projection — lowercased / typo / unrelated — and the error
1248    /// echoes the input verbatim so the operator-facing diagnostic
1249    /// carries the offending value, not a normalized form. The
1250    /// empty-input arm is pinned by
1251    /// [`report_format_is_well_formed_closed_set`] via the
1252    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1253    /// verbatim-echo contract on the [`UnknownReportFormat`] newtype,
1254    /// which the trait's `make_unknown` can't see.
1255    #[test]
1256    fn unknown_report_format_errors() {
1257        use std::str::FromStr;
1258        for bad in ["junit", "JUNIT", "tap", "Yaml", "TomlV1"] {
1259            let err = ReportFormat::from_str(bad).unwrap_err();
1260            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1261        }
1262    }
1263
1264    // `unknown_report_format_message_matches_substrate_convention`
1265    // removed — clause (5) of
1266    // `tatara_closed_set::assert_closed_set_well_formed::<ReportFormat>()`
1267    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
1268    // shape generically (called from `report_format_is_well_formed_closed_set`
1269    // above); the `SET_LABEL` projection is pinned by
1270    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
1271
1272    /// TRUTH-TABLE CONTRACT: `payload_shape` agrees with the documented
1273    /// shape table for every variant in `ALL`. A new variant whose
1274    /// shape the author forgets to add to `payload_shape`'s match is
1275    /// caught by the compiler at the match site; a regression that
1276    /// reshuffles existing variants (e.g. routing `NdJson` to opaque
1277    /// bytes) is caught here. `payload_shape` is the worker's only
1278    /// dispatch — once this passes, the worker's `match shape { … }`
1279    /// is exhaustive on the 2-variant `ReportPayloadShape` instead of
1280    /// the 4-variant `ReportFormat`, so adding a future format never
1281    /// touches the worker.
1282    #[test]
1283    fn report_format_payload_shape_truth_table() {
1284        let table: &[(ReportFormat, ReportPayloadShape)] = &[
1285            (ReportFormat::Junit, ReportPayloadShape::OpaqueBytes),
1286            (ReportFormat::TapV13, ReportPayloadShape::OpaqueBytes),
1287            (ReportFormat::NdJson, ReportPayloadShape::NdJsonLines),
1288            (ReportFormat::Raw, ReportPayloadShape::OpaqueBytes),
1289        ];
1290        assert_eq!(table.len(), ReportFormat::ALL.len());
1291        for (format, expected) in table {
1292            assert_eq!(
1293                format.payload_shape(),
1294                *expected,
1295                "payload_shape({format:?}) drift"
1296            );
1297        }
1298    }
1299
1300    /// CLOSURE-OF-PROJECTION CONTRACT: every `ReportPayloadShape`
1301    /// variant is the image of at least one `ReportFormat` variant —
1302    /// no shape is stranded. A `Compressed` shape added to
1303    /// `ReportPayloadShape::ALL` without an `ALL → payload_shape`
1304    /// mapping at any `ReportFormat` arm makes the worker's
1305    /// 3-variant dispatch reachable from no input, which would
1306    /// silently dead-code one arm. Caught here.
1307    #[test]
1308    fn report_payload_shape_reachable_from_some_report_format() {
1309        for shape in ReportPayloadShape::ALL {
1310            let reachable = ReportFormat::ALL.iter().any(|f| f.payload_shape() == shape);
1311            assert!(
1312                reachable,
1313                "{shape:?} is in ReportPayloadShape::ALL but no ReportFormat projects to it"
1314            );
1315        }
1316    }
1317
1318    /// CLOSED-SET CONTRACT: `ReportPayloadShape::ALL` enumerates each
1319    /// variant exactly once. The `[Self; 2]` array literal forces
1320    /// the arity at compile time; this test pins per-variant
1321    /// reachability so adding a third shape (`Compressed`) without
1322    /// extending `ALL` fails here rather than silently dropping the
1323    /// new variant from every sweep through `Self::ALL`.
1324    #[test]
1325    fn report_payload_shape_all_enumerates_each_variant_exactly_once() {
1326        let mut seen = std::collections::HashSet::new();
1327        for shape in ReportPayloadShape::ALL {
1328            assert!(seen.insert(shape), "duplicate variant in ALL: {shape:?}");
1329        }
1330        assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
1331        for shape in [
1332            ReportPayloadShape::NdJsonLines,
1333            ReportPayloadShape::OpaqueBytes,
1334        ] {
1335            assert!(
1336                ReportPayloadShape::ALL.contains(&shape),
1337                "{shape:?} declared but not in ALL"
1338            );
1339        }
1340    }
1341
1342    /// CANONICAL-KEY UNIQUENESS: no two shapes alias the same
1343    /// `as_str` identifier. A future rename of one variant to a name
1344    /// that collides with another (e.g. both → `"Lines"`) breaks the
1345    /// shape's identity in operator-facing reason strings and would
1346    /// silently make Display non-injective. Caught here.
1347    #[test]
1348    fn report_payload_shape_as_str_unique_per_variant() {
1349        let mut seen = std::collections::HashSet::new();
1350        for shape in ReportPayloadShape::ALL {
1351            assert!(
1352                seen.insert(shape.as_str()),
1353                "as_str collision: {shape:?} → {:?}",
1354                shape.as_str()
1355            );
1356        }
1357        assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
1358    }
1359
1360    /// DISPLAY-IS-AS_STR: the Display impl IS `as_str` — pinning
1361    /// this lets callers reach for either projection without drift.
1362    /// Sibling to `report_format_display_matches_as_str` and
1363    /// `export_trigger_display_matches_as_str`. Routed through the
1364    /// substrate-wide [`crate::tagged_union::assert_display_matches_label`]
1365    /// primitive so the sweep body lives at ONE substrate site rather
1366    /// than restated per-implementor. Also exercised through the
1367    /// substrate-wide `every_production_display_impl_binds_through_the_testkit_primitive`
1368    /// sweep so a per-crate test-site drop cannot silently disable the
1369    /// check.
1370    #[test]
1371    fn report_payload_shape_display_matches_as_str() {
1372        crate::tagged_union::assert_display_matches_label::<ReportPayloadShape>();
1373    }
1374
1375    /// EMBED-FIELD UNIQUENESS: no two shapes write into the same
1376    /// `payload.<field>` key. The worker's embed site is
1377    /// `payload.insert(shape.payload_field().into(), …)`; if two
1378    /// shapes aliased to the same field name, two different report
1379    /// sources arriving in the same export envelope would silently
1380    /// overwrite each other's bytes. Caught here.
1381    #[test]
1382    fn report_payload_shape_payload_field_unique_per_variant() {
1383        let mut seen = std::collections::HashSet::new();
1384        for shape in ReportPayloadShape::ALL {
1385            assert!(
1386                seen.insert(shape.payload_field()),
1387                "payload_field collision: {shape:?} → {:?}",
1388                shape.payload_field()
1389            );
1390        }
1391        assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
1392    }
1393
1394    /// TRUTH-TABLE: `payload_field` matches the documented
1395    /// `payload.ndjson` / `payload.raw_b64` shinryu schema. A future
1396    /// rename (e.g. `"raw_b64"` → `"raw"`) lands here at one arm
1397    /// rather than drifting between the docstring prose and the
1398    /// worker's embed-site literal. Adding a third shape forces the
1399    /// author to add a row here (driven by `ALL`), so the table
1400    /// width tracks the closed set.
1401    #[test]
1402    fn report_payload_shape_payload_field_truth_table() {
1403        let table: &[(ReportPayloadShape, &str)] = &[
1404            (ReportPayloadShape::NdJsonLines, "ndjson"),
1405            (ReportPayloadShape::OpaqueBytes, "raw_b64"),
1406        ];
1407        assert_eq!(table.len(), ReportPayloadShape::ALL.len());
1408        for (shape, expected) in table {
1409            assert_eq!(
1410                shape.payload_field(),
1411                *expected,
1412                "payload_field({shape:?}) drift"
1413            );
1414        }
1415    }
1416
1417    /// Every variant's `payload_field` is non-empty and contains no
1418    /// JSON-path-separator (`.`) — the worker concatenates
1419    /// `payload.<payload_field>` so an embedded `.` would alias into
1420    /// the parent map and silently flatten the embed. Structural
1421    /// guard for the field-name shape.
1422    #[test]
1423    fn report_payload_shape_payload_field_is_a_single_segment() {
1424        for shape in ReportPayloadShape::ALL {
1425            let field = shape.payload_field();
1426            assert!(
1427                !field.is_empty(),
1428                "payload_field({shape:?}) is empty — embed site has no destination"
1429            );
1430            assert!(
1431                !field.contains('.'),
1432                "payload_field({shape:?}) contains '.' ({field:?}) — would flatten the embed into payload's parent map"
1433            );
1434        }
1435    }
1436
1437    #[test]
1438    fn export_spec_serde_round_trip() {
1439        let spec = ExportSpec {
1440            source: ArtifactSource {
1441                test_report: Some(TestReportSource {
1442                    configmap: "demo-test-results".into(),
1443                    key: "junit.xml".into(),
1444                    format: ReportFormat::Junit,
1445                    namespace: None,
1446                }),
1447                ..ArtifactSource::default()
1448            },
1449            channel: VectorChannel {
1450                http_event: Some(HttpEventChannel::signal("test-report")),
1451                ..VectorChannel::default()
1452            },
1453            when: ExportTrigger::Always,
1454            experiment_id_override: Some("demo-run-2026-05-20".into()),
1455        };
1456
1457        let yaml = serde_yaml::to_string(&spec).unwrap();
1458        // camelCase wire format — what FluxCD / kubectl users see.
1459        assert!(yaml.contains("source:"));
1460        assert!(yaml.contains("testReport:"));
1461        assert!(yaml.contains("configmap: demo-test-results"));
1462        assert!(yaml.contains("format: Junit"));
1463        assert!(yaml.contains("channel:"));
1464        assert!(yaml.contains("httpEvent:"));
1465        assert!(yaml.contains("signalType: test-report"));
1466        assert!(yaml.contains("when: Always"));
1467        assert!(yaml.contains("experimentIdOverride: demo-run-2026-05-20"));
1468
1469        let back: ExportSpec = serde_yaml::from_str(&yaml).unwrap();
1470        assert!(back.source.test_report.is_some());
1471        assert!(back.channel.http_event.is_some());
1472        assert_eq!(back.when, ExportTrigger::Always);
1473    }
1474
1475    #[test]
1476    fn run_marker_labels_round_trip() {
1477        let mut labels = BTreeMap::new();
1478        labels.insert("run-id".into(), "demo-run-2026-05-20".into());
1479        labels.insert("phase".into(), "end".into());
1480        let spec = ExportSpec {
1481            source: ArtifactSource {
1482                run_marker: Some(RunMarkerSource { labels }),
1483                ..ArtifactSource::default()
1484            },
1485            channel: VectorChannel {
1486                http_event: Some(HttpEventChannel::signal("ephemeral-marker")),
1487                ..VectorChannel::default()
1488            },
1489            when: ExportTrigger::Always,
1490            experiment_id_override: None,
1491        };
1492        let yaml = serde_yaml::to_string(&spec).unwrap();
1493        assert!(yaml.contains("runMarker:"));
1494        assert!(yaml.contains("run-id: demo-run-2026-05-20"));
1495        let back: ExportSpec = serde_yaml::from_str(&yaml).unwrap();
1496        let rm = back.source.run_marker.unwrap();
1497        assert_eq!(rm.labels["phase"], "end");
1498    }
1499
1500    /// Default endpoints resolve to the canonical in-cluster Service
1501    /// DNS — a single source of truth other tatara crates can
1502    /// re-export instead of duplicating literals.
1503    #[test]
1504    fn default_endpoints_are_stable_constants() {
1505        assert_eq!(
1506            DEFAULT_VECTOR_INGEST,
1507            "http://vector.observability.svc.cluster.local:8080"
1508        );
1509        assert_eq!(
1510            DEFAULT_NATS_URL,
1511            "nats://nats.observability.svc.cluster.local:4222"
1512        );
1513    }
1514
1515    // ── HttpEventChannel::signal substrate primitive pins ────────────
1516    //
1517    // Sweeps the wire-shape corners the ten pre-lift `HttpEventChannel {
1518    // endpoint: None, signal_type: <s>.into() }` hand-authored fixture
1519    // literals covered — the default-endpoint sentinel projects to
1520    // `None`, the `signal_type` slot rides through verbatim, and the
1521    // downstream [`HttpEventChannel::resolved_endpoint`] read still
1522    // resolves to [`DEFAULT_VECTOR_INGEST`]. A regression that seeded
1523    // a hardcoded endpoint on the composer, dropped the `Into<String>`
1524    // bound so a `&str` caller has to `.to_string()` per-site, or
1525    // reordered the two struct slots surfaces here rather than as
1526    // silent skew at any of the ten downstream fixtures (five in
1527    // `tatara-process::export`, two in `tatara-process::lifetime`, two
1528    // in `tatara-process::tagged_union`, one in
1529    // `tatara-reconciler::render`, one in `tatara-export-worker::lib`).
1530
1531    /// PRIMARY SHAPE: byte-identical parity with the pre-lift 4-token
1532    /// hand-authored literal every fixture spelled. Sweeps every label
1533    /// the ten collapsed sites carry so a regression that dropped or
1534    /// mutated any observable slot's value surfaces HERE rather than
1535    /// downstream. The `endpoint: None` sentinel is the load-bearing
1536    /// slot ([`HttpEventChannel::resolved_endpoint`] gates on `.is_none()`
1537    /// to reach [`DEFAULT_VECTOR_INGEST`]); the pin binds it before the
1538    /// primitive can drift.
1539    #[test]
1540    fn signal_composes_byte_identical_to_pre_lift_literal_across_every_label() {
1541        for label in [
1542            "receipt",
1543            "test-report",
1544            "ephemeral-marker",
1545            "x",
1546            "s",
1547            "demo-run-2026-05-20",
1548        ] {
1549            let via_primitive = HttpEventChannel::signal(label);
1550            let hand_authored = HttpEventChannel {
1551                endpoint: None,
1552                signal_type: label.to_string(),
1553            };
1554            assert_eq!(
1555                via_primitive.endpoint, hand_authored.endpoint,
1556                "signal must project the endpoint slot byte-identically \
1557                 to the pre-lift literal on label={label:?}",
1558            );
1559            assert_eq!(
1560                via_primitive.signal_type, hand_authored.signal_type,
1561                "signal must project the signal_type slot byte-identically \
1562                 to the pre-lift literal on label={label:?}",
1563            );
1564            assert!(
1565                via_primitive.endpoint.is_none(),
1566                "signal must stamp endpoint: None so resolved_endpoint \
1567                 reaches DEFAULT_VECTOR_INGEST on label={label:?}",
1568            );
1569            assert_eq!(
1570                via_primitive.resolved_endpoint(),
1571                DEFAULT_VECTOR_INGEST,
1572                "signal must compose with resolved_endpoint's \
1573                 default-fallback gate on label={label:?}",
1574            );
1575        }
1576    }
1577
1578    /// COERCION AXIS PIN: the `impl Into<String>` bound accepts every
1579    /// pre-lift caller shape without a per-site coercion. Pre-lift the
1580    /// ten sites carried three distinct source shapes for the
1581    /// `signal_type` slot: `&'static str` literals with `.into()`
1582    /// (`"receipt".into()`), the export-worker fixture's owned
1583    /// `String` via `.to_string()` (`signal_type.to_string()`), and
1584    /// the property-probe fixtures' short single-char labels. Post-lift
1585    /// EVERY shape reaches the composer through the same `Into<String>`
1586    /// gate; the pin binds that so a future narrowing to `&str` (which
1587    /// would break the export-worker's `signal_type: &str` parameter
1588    /// shape) surfaces here.
1589    #[test]
1590    fn signal_accepts_every_pre_lift_caller_source_shape() {
1591        // Shape 1: `&'static str` literal — every test-fixture site.
1592        let a = HttpEventChannel::signal("receipt");
1593        assert_eq!(a.signal_type, "receipt");
1594        // Shape 2: owned `String` — the export-worker `http_spec`
1595        // fixture pre-lift spelled `signal_type: signal_type.to_string()`
1596        // to project its `&str` parameter into the slot.
1597        let owned: String = "test-report".to_string();
1598        let b = HttpEventChannel::signal(owned);
1599        assert_eq!(b.signal_type, "test-report");
1600        // Shape 3: `&String` — verifies the `Into<String>` bound
1601        // accepts a borrowed owned string without an explicit clone
1602        // (matches the reference shape a caller might reach for after
1603        // an intermediate `let label = String::from("...");` binding).
1604        let borrowed = String::from("ephemeral-marker");
1605        let c = HttpEventChannel::signal(&borrowed[..]);
1606        assert_eq!(c.signal_type, "ephemeral-marker");
1607    }
1608
1609    /// COMPOSITION PIN: `HttpEventChannel::signal` composes byte-
1610    /// identically with [`ChannelKind::select`] on the resolver axis —
1611    /// wrapping the primitive's output in the `VectorChannel` tagged-
1612    /// union slot yields the same `ChannelVariant::HttpEvent(...)`
1613    /// projection as the pre-lift literal did. Guards the primary
1614    /// downstream consumer (the tagged-union `.variant()` resolver
1615    /// every fixture round-trips through) against a regression that
1616    /// projected the primitive onto a non-http-event slot or dropped
1617    /// its `endpoint`/`signal_type` slots between composition sites.
1618    #[test]
1619    fn signal_composes_with_channel_variant_resolver() {
1620        let c = VectorChannel {
1621            http_event: Some(HttpEventChannel::signal("receipt")),
1622            ..VectorChannel::default()
1623        };
1624        match c.variant().unwrap() {
1625            ChannelVariant::HttpEvent(h) => {
1626                assert_eq!(h.signal_type, "receipt");
1627                assert_eq!(h.resolved_endpoint(), DEFAULT_VECTOR_INGEST);
1628                assert!(h.endpoint.is_none());
1629            }
1630            other => panic!("expected HttpEvent, got {other:?}"),
1631        }
1632        // Kind projection through the closed-set discriminator stays
1633        // coherent with `ChannelKind::HttpEvent` — a regression that
1634        // wired the primitive to a non-http-event slot would surface
1635        // here as a wrong-kind panic before any downstream test firing.
1636        let via_kind = ChannelKind::HttpEvent.select(&c).unwrap();
1637        assert_eq!(via_kind.kind(), ChannelKind::HttpEvent);
1638    }
1639
1640    // ── closed-set algebra for ArtifactKind (ALL × as_str × Display ×
1641    //    FromStr × select × ArtifactVariant::kind) ─────────────────────
1642
1643    /// Structural well-formedness of [`ArtifactKind`] as a
1644    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1645    /// testkit lift that pins all three structural invariants (`ALL`
1646    /// is non-empty, every variant round-trips through
1647    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1648    /// outside the closed set) at ONE call site. Replaces the hand-
1649    /// derived `artifact_kind_all_is_unique_and_complete` +
1650    /// `artifact_kind_roundtrip_via_as_str` + the empty-input arm of
1651    /// `unknown_artifact_kind_errors`. `FromStr` delegates to
1652    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1653    /// exercises the same code path the export worker hits when
1654    /// parsing a CRD `enum:`-validated value back to the typed kind.
1655    #[test]
1656    fn artifact_kind_is_well_formed_closed_set() {
1657        tatara_closed_set::assert_closed_set_well_formed::<ArtifactKind>();
1658    }
1659
1660    /// CANONICAL-KEY CONTRACT: every `ArtifactKind::as_str()` matches
1661    /// the serde `rename_all = "camelCase"` field name on the
1662    /// corresponding `Option<…>` slot of `ArtifactSource`. A future
1663    /// rename of either the struct field OR the `as_str` arm lands
1664    /// here at one site, instead of drifting between the typed
1665    /// surface, the YAML wire format, and the `ArtifactError::Empty`
1666    /// diagnostic. The mapping is the table the serde derive produces
1667    /// against the struct field declarations above; reading the YAML
1668    /// output pins it without re-deriving by hand.
1669    ///
1670    /// Routes through the substrate primitive
1671    /// [`crate::tagged_union::assert_single_slot_key_matches_label`],
1672    /// which pins the exactly-one-key + name-equality projection
1673    /// byte-identically for every `<T: TaggedUnion + Serialize>`
1674    /// implementor — the wire-alignment testkit shared with the sibling
1675    /// `intent_kind_as_str_matches_intent_field_name` /
1676    /// `encapsulation_target_as_str_matches_field_name` /
1677    /// `channel_kind_as_str_matches_field_name` sites. Pre-lift this
1678    /// site restated a weaker YAML-substring check (`yaml.contains(&format!("{key}:"))`)
1679    /// which would silently pass on drift where a non-tagged-union
1680    /// field was added to `ArtifactSource`; post-lift the primitive's
1681    /// JSON exactly-one form catches that drift too — at ONE substrate
1682    /// site.
1683    #[test]
1684    fn artifact_kind_as_str_matches_field_name() {
1685        crate::tagged_union::assert_single_slot_key_matches_label::<ArtifactSource, _>(
1686            single_slot_source,
1687        );
1688    }
1689
1690    /// CANONICAL-NAMES PIN: byte-exact camelCase wire-format pin —
1691    /// renaming any of these strings IS a wire-format break that fails
1692    /// this test FIRST so the rename stays a deliberate decision, not
1693    /// a typo. Locks the (variant → operator-facing key) table.
1694    #[test]
1695    fn artifact_kind_canonical_names_pinned() {
1696        assert_eq!(ArtifactKind::Receipts.as_str(), "receipts");
1697        assert_eq!(ArtifactKind::TestReport.as_str(), "testReport");
1698        assert_eq!(ArtifactKind::ProcessSnapshot.as_str(), "processSnapshot");
1699        assert_eq!(ArtifactKind::RunMarker.as_str(), "runMarker");
1700    }
1701
1702    /// The Display impl IS `as_str` — pinning this lets future callers
1703    /// reach for either projection without drift. If a reviewer
1704    /// accidentally re-introduces an inline match in Display, this
1705    /// test would fail the moment a variant rename touches one site
1706    /// but not the other.
1707    #[test]
1708    fn artifact_kind_display_matches_as_str() {
1709        crate::tagged_union::assert_display_matches_label::<ArtifactKind>();
1710    }
1711
1712    /// `FromStr` rejects strings that aren't in the canonical
1713    /// projection — PascalCased / typo / cross-axis-leaked inputs
1714    /// from sibling closed-set enums on the same `ExportSpec` axis
1715    /// (`Junit`, `OnAttested`, …) — and the error echoes the input
1716    /// verbatim so the operator-facing diagnostic carries the
1717    /// offending value, not a normalized form. `ArtifactKind` is its
1718    /// own axis, NOT a transparent reflection of any sibling. The
1719    /// empty-input arm is pinned by
1720    /// [`artifact_kind_is_well_formed_closed_set`] via the
1721    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1722    /// verbatim-echo contract on the [`UnknownArtifactKind`] newtype,
1723    /// which the trait's `make_unknown` can't see.
1724    #[test]
1725    fn unknown_artifact_kind_errors() {
1726        use std::str::FromStr;
1727        for bad in [
1728            "Receipts",
1729            "test_report",
1730            "RECEIPTS",
1731            "snapshot",
1732            "marker",
1733            "Junit",
1734            "OnAttested",
1735            "NdJsonLines",
1736        ] {
1737            let err = ArtifactKind::from_str(bad).unwrap_err();
1738            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1739        }
1740    }
1741
1742    /// ROUND-TRIP CONTRACT: every kind reaches its borrowed-variant
1743    /// view via `select`, and that variant projects back to the same
1744    /// kind via `ArtifactVariant::kind`. A regression that misroutes a
1745    /// select arm (e.g. `Self::Receipts => source.test_report.as_ref()
1746    /// ...`) fails loudly here.
1747    ///
1748    /// Routes through the substrate primitive
1749    /// [`crate::tagged_union::assert_variant_round_trip`] shared with
1750    /// the sibling `intent_kind_round_trips_through_variant_kind` /
1751    /// `channel_kind_round_trips_through_variant_kind` /
1752    /// `encapsulation_target_round_trips_through_variant_target`
1753    /// sites — the projection lives at ONE substrate primitive and
1754    /// every site binds through a single call.
1755    #[test]
1756    fn artifact_kind_round_trips_through_variant_kind() {
1757        crate::tagged_union::assert_variant_round_trip::<ArtifactSource, _>(single_slot_source);
1758    }
1759
1760    /// SELECT-EMPTY CONTRACT: an unpopulated slot returns `None` from
1761    /// `select`, for every kind. Pairs with the resolver's `Empty`
1762    /// path so a future kind's slot defaulting wrong (e.g. accidentally
1763    /// `Some(Default::default())` instead of `None`) is caught here.
1764    #[test]
1765    fn artifact_kind_select_returns_none_for_unset_slot() {
1766        let empty = ArtifactSource::default();
1767        for kind in ArtifactKind::ALL {
1768            assert!(
1769                kind.select(&empty).is_none(),
1770                "{kind:?} reported populated on a default ArtifactSource"
1771            );
1772        }
1773    }
1774
1775    /// EMPTY-DIAGNOSTIC CONTRACT: the closed-set kind list embedded
1776    /// in `ArtifactError::Empty` echoes the canonical join of every
1777    /// `ArtifactKind::as_str()` projection. A variant added without
1778    /// updating `ARTIFACT_KIND_LIST` (or a renamed variant) shows up
1779    /// here as a mismatch. Routes through the substrate primitive
1780    /// [`crate::tagged_union::assert_kind_list_matches_closed_set`]
1781    /// shared with the sibling
1782    /// `intent_error_empty_lists_every_kind_in_canonical_order`
1783    /// / `channel_error_empty_lists_every_kind_in_canonical_order`
1784    /// / `encapsulation_kind_error_empty_lists_every_target_in_canonical_order`
1785    /// sites — the projection lives at ONE substrate primitive and
1786    /// every site binds through a single call.
1787    #[test]
1788    fn artifact_error_empty_lists_every_kind_in_canonical_order() {
1789        crate::tagged_union::assert_kind_list_matches_closed_set::<ArtifactSource>();
1790    }
1791
1792    /// AMBIGUOUS-PATH CONTRACT: when two slots are populated the
1793    /// resolver yields `Ambiguous`, exhaustively across every pair in
1794    /// `ALL × ALL` (excluding the diagonal). A future asymmetry where
1795    /// one slot would silently shadow another (e.g. an `if-let` chain
1796    /// re-introducing first-wins ordering) is caught here. Routes
1797    /// through the substrate primitive
1798    /// [`crate::tagged_union::assert_two_slots_ambiguous`] shared with
1799    /// the sibling
1800    /// `encapsulation_kind_two_slots_is_ambiguous_across_every_pair`
1801    /// / `vector_channel_two_slots_is_ambiguous_across_every_pair`
1802    /// / `intent_two_slots_is_ambiguous_across_every_pair` sites — the
1803    /// nested-`for a in K::ALL { for b in K::ALL { … } }` sweep lives
1804    /// at ONE substrate site.
1805    #[test]
1806    fn artifact_source_two_slots_is_ambiguous_across_every_pair() {
1807        crate::tagged_union::assert_two_slots_ambiguous::<ArtifactSource, _>(two_slot_source);
1808    }
1809
1810    // ── closed-set algebra for ChannelKind (ALL × as_str × Display ×
1811    //    FromStr × select × ChannelVariant::kind) ─────────────────────
1812
1813    /// Structural well-formedness of [`ChannelKind`] as a
1814    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1815    /// testkit lift that pins all three structural invariants (`ALL`
1816    /// is non-empty, every variant round-trips through
1817    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1818    /// outside the closed set) at ONE call site. Replaces the hand-
1819    /// derived `channel_kind_all_is_unique_and_complete` +
1820    /// `channel_kind_roundtrip_via_as_str` + the empty-input arm of
1821    /// `unknown_channel_kind_errors`. `FromStr` delegates to
1822    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1823    /// exercises the same code path the export worker hits when
1824    /// parsing a CRD `enum:`-validated value back to the typed kind.
1825    #[test]
1826    fn channel_kind_is_well_formed_closed_set() {
1827        tatara_closed_set::assert_closed_set_well_formed::<ChannelKind>();
1828    }
1829
1830    /// CANONICAL-KEY CONTRACT: every `ChannelKind::as_str()` matches
1831    /// the serde `rename_all = "camelCase"` field name on the
1832    /// corresponding `Option<…>` slot of `VectorChannel`. A future
1833    /// rename of either the struct field OR the `as_str` arm lands
1834    /// here at one site, instead of drifting between the typed
1835    /// surface, the YAML wire format, and the `ChannelError::Empty`
1836    /// diagnostic.
1837    ///
1838    /// Routes through the substrate primitive
1839    /// [`crate::tagged_union::assert_single_slot_key_matches_label`],
1840    /// which pins the exactly-one-key + name-equality projection
1841    /// byte-identically for every `<T: TaggedUnion + Serialize>`
1842    /// implementor — the wire-alignment testkit shared with the sibling
1843    /// `intent_kind_as_str_matches_intent_field_name` /
1844    /// `encapsulation_target_as_str_matches_field_name` /
1845    /// `artifact_kind_as_str_matches_field_name` sites. Pre-lift this
1846    /// site restated a weaker YAML-substring check (`yaml.contains(&format!("{key}:"))`)
1847    /// which would silently pass on drift where a non-tagged-union
1848    /// field was added to `VectorChannel`; post-lift the primitive's
1849    /// JSON exactly-one form catches that drift too — at ONE substrate
1850    /// site.
1851    #[test]
1852    fn channel_kind_as_str_matches_field_name() {
1853        crate::tagged_union::assert_single_slot_key_matches_label::<VectorChannel, _>(
1854            single_slot_channel,
1855        );
1856    }
1857
1858    /// CANONICAL-NAMES PIN: byte-exact camelCase wire-format pin —
1859    /// renaming any of these strings IS a wire-format break that fails
1860    /// this test FIRST so the rename stays a deliberate decision, not
1861    /// a typo. Locks the (variant → operator-facing key) table.
1862    #[test]
1863    fn channel_kind_canonical_names_pinned() {
1864        assert_eq!(ChannelKind::HttpEvent.as_str(), "httpEvent");
1865        assert_eq!(ChannelKind::NatsSubject.as_str(), "natsSubject");
1866        assert_eq!(ChannelKind::Stdout.as_str(), "stdout");
1867    }
1868
1869    /// The Display impl IS `as_str` — pinning this lets future callers
1870    /// reach for either projection without drift.
1871    #[test]
1872    fn channel_kind_display_matches_as_str() {
1873        crate::tagged_union::assert_display_matches_label::<ChannelKind>();
1874    }
1875
1876    /// `FromStr` rejects strings that aren't in the canonical
1877    /// projection — PascalCased / typo / cross-axis-leaked inputs
1878    /// from sibling closed-set enums on the same `ExportSpec` axis
1879    /// (`Receipts`, `OnAttested`, `Junit`, …) — and the error echoes
1880    /// the input verbatim so the operator-facing diagnostic carries
1881    /// the offending value, not a normalized form. `ChannelKind` is
1882    /// its own axis, NOT a transparent reflection of any sibling. The
1883    /// empty-input arm is pinned by
1884    /// [`channel_kind_is_well_formed_closed_set`] via the
1885    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1886    /// verbatim-echo contract on the [`UnknownChannelKind`] newtype,
1887    /// which the trait's `make_unknown` can't see.
1888    #[test]
1889    fn unknown_channel_kind_errors() {
1890        use std::str::FromStr;
1891        for bad in [
1892            "HttpEvent",
1893            "http_event",
1894            "HTTPEVENT",
1895            "nats",
1896            "STDOUT",
1897            "Receipts",
1898            "OnAttested",
1899            "Junit",
1900            "NdJsonLines",
1901        ] {
1902            let err = ChannelKind::from_str(bad).unwrap_err();
1903            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1904        }
1905    }
1906
1907    // `unknown_channel_kind_message_matches_substrate_convention`
1908    // removed — clause (5) of
1909    // `tatara_closed_set::assert_closed_set_well_formed::<ChannelKind>()`
1910    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
1911    // shape generically (called from `channel_kind_is_well_formed_closed_set`
1912    // above); the `SET_LABEL` projection is pinned by
1913    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
1914
1915    /// ROUND-TRIP CONTRACT: every kind reaches its borrowed-variant
1916    /// view via `select`, and that variant projects back to the same
1917    /// kind via `ChannelVariant::kind`. A regression that misroutes a
1918    /// select arm (e.g. `Self::HttpEvent => channel.nats_subject ...`)
1919    /// fails loudly here.
1920    ///
1921    /// Routes through the substrate primitive
1922    /// [`crate::tagged_union::assert_variant_round_trip`] shared with
1923    /// the sibling `intent_kind_round_trips_through_variant_kind` /
1924    /// `artifact_kind_round_trips_through_variant_kind` /
1925    /// `encapsulation_target_round_trips_through_variant_target`
1926    /// sites — the projection lives at ONE substrate primitive and
1927    /// every site binds through a single call.
1928    #[test]
1929    fn channel_kind_round_trips_through_variant_kind() {
1930        crate::tagged_union::assert_variant_round_trip::<VectorChannel, _>(single_slot_channel);
1931    }
1932
1933    /// SELECT-EMPTY CONTRACT: an unpopulated slot returns `None` from
1934    /// `select`, for every kind. Pairs with the resolver's `Empty`
1935    /// path so a future kind's slot defaulting wrong (e.g. accidentally
1936    /// `Some(Default::default())` instead of `None`) is caught here.
1937    #[test]
1938    fn channel_kind_select_returns_none_for_unset_slot() {
1939        let empty = VectorChannel::default();
1940        for kind in ChannelKind::ALL {
1941            assert!(
1942                kind.select(&empty).is_none(),
1943                "{kind:?} reported populated on a default VectorChannel"
1944            );
1945        }
1946    }
1947
1948    /// EMPTY-DIAGNOSTIC CONTRACT: the closed-set kind list embedded
1949    /// in `ChannelError::Empty` echoes the canonical join of every
1950    /// `ChannelKind::as_str()` projection. A variant added without
1951    /// updating `CHANNEL_KIND_LIST` (or a renamed variant) shows up
1952    /// here as a mismatch. Routes through the substrate primitive
1953    /// [`crate::tagged_union::assert_kind_list_matches_closed_set`]
1954    /// shared with the sibling `intent_error_empty_lists_every_kind_in_canonical_order`
1955    /// / `artifact_error_empty_lists_every_kind_in_canonical_order`
1956    /// / `encapsulation_kind_error_empty_lists_every_target_in_canonical_order`
1957    /// sites — the projection lives at ONE substrate primitive and
1958    /// every site binds through a single call.
1959    #[test]
1960    fn channel_error_empty_lists_every_kind_in_canonical_order() {
1961        crate::tagged_union::assert_kind_list_matches_closed_set::<VectorChannel>();
1962    }
1963
1964    /// AMBIGUOUS-PATH CONTRACT: when two slots are populated the
1965    /// resolver yields `Ambiguous`, exhaustively across every pair in
1966    /// `ALL × ALL` (excluding the diagonal). A future asymmetry where
1967    /// one slot would silently shadow another (e.g. an `if-let` chain
1968    /// re-introducing first-wins ordering) is caught here. Routes
1969    /// through the substrate primitive
1970    /// [`crate::tagged_union::assert_two_slots_ambiguous`] shared with
1971    /// the sibling
1972    /// `encapsulation_kind_two_slots_is_ambiguous_across_every_pair`
1973    /// / `artifact_source_two_slots_is_ambiguous_across_every_pair`
1974    /// / `intent_two_slots_is_ambiguous_across_every_pair` sites — the
1975    /// nested-`for a in K::ALL { for b in K::ALL { … } }` sweep lives
1976    /// at ONE substrate site.
1977    #[test]
1978    fn vector_channel_two_slots_is_ambiguous_across_every_pair() {
1979        crate::tagged_union::assert_two_slots_ambiguous::<VectorChannel, _>(two_slot_channel);
1980    }
1981
1982    /// Construct a `VectorChannel` with exactly the given kind's slot
1983    /// populated by a minimal valid inner channel. Shared across the
1984    /// closed-set property tests so they each cover every variant
1985    /// without restating the construction table. Mirrors
1986    /// `single_slot_source` in shape.
1987    fn single_slot_channel(kind: ChannelKind) -> VectorChannel {
1988        match kind {
1989            ChannelKind::HttpEvent => VectorChannel {
1990                http_event: Some(HttpEventChannel::signal("x")),
1991                ..VectorChannel::default()
1992            },
1993            ChannelKind::NatsSubject => VectorChannel {
1994                nats_subject: Some(NatsSubjectChannel {
1995                    subject: "s".into(),
1996                    stream: "S".into(),
1997                    url: None,
1998                }),
1999                ..VectorChannel::default()
2000            },
2001            ChannelKind::Stdout => VectorChannel {
2002                stdout: Some(StdoutChannel::default()),
2003                ..VectorChannel::default()
2004            },
2005        }
2006    }
2007
2008    /// Construct a `VectorChannel` with two slots populated — drives
2009    /// the pairwise `Ambiguous` sweep. Composes the single-slot
2010    /// constructor on top of itself to keep one source of truth for
2011    /// per-variant inner payloads.
2012    fn two_slot_channel(a: ChannelKind, b: ChannelKind) -> VectorChannel {
2013        let ca = single_slot_channel(a);
2014        let cb = single_slot_channel(b);
2015        VectorChannel {
2016            http_event: ca.http_event.or(cb.http_event),
2017            nats_subject: ca.nats_subject.or(cb.nats_subject),
2018            stdout: ca.stdout.or(cb.stdout),
2019        }
2020    }
2021
2022    /// Construct an `ArtifactSource` with exactly the given kind's
2023    /// slot populated by a minimal valid inner source. Shared across
2024    /// the closed-set property tests so they each cover every variant
2025    /// without restating the construction table. Mirrors
2026    /// `single_slot_intent` in shape.
2027    fn single_slot_source(kind: ArtifactKind) -> ArtifactSource {
2028        match kind {
2029            ArtifactKind::Receipts => ArtifactSource {
2030                receipts: Some(ReceiptsSource::default()),
2031                ..ArtifactSource::default()
2032            },
2033            ArtifactKind::TestReport => ArtifactSource {
2034                test_report: Some(TestReportSource {
2035                    configmap: "cm".into(),
2036                    key: "k".into(),
2037                    format: ReportFormat::Junit,
2038                    namespace: None,
2039                }),
2040                ..ArtifactSource::default()
2041            },
2042            ArtifactKind::ProcessSnapshot => ArtifactSource {
2043                process_snapshot: Some(ProcessSnapshotSource::default()),
2044                ..ArtifactSource::default()
2045            },
2046            ArtifactKind::RunMarker => ArtifactSource {
2047                run_marker: Some(RunMarkerSource::default()),
2048                ..ArtifactSource::default()
2049            },
2050        }
2051    }
2052
2053    /// Construct an `ArtifactSource` with two slots populated — drives
2054    /// the pairwise `Ambiguous` sweep. Composes the single-slot
2055    /// constructor on top of itself to keep one source of truth for
2056    /// per-variant inner payloads.
2057    fn two_slot_source(a: ArtifactKind, b: ArtifactKind) -> ArtifactSource {
2058        // Merge by populating each kind's slot from its single-slot view.
2059        let sa = single_slot_source(a);
2060        let sb = single_slot_source(b);
2061        ArtifactSource {
2062            receipts: sa.receipts.or(sb.receipts),
2063            test_report: sa.test_report.or(sb.test_report),
2064            process_snapshot: sa.process_snapshot.or(sb.process_snapshot),
2065            run_marker: sa.run_marker.or(sb.run_marker),
2066        }
2067    }
2068}