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 /// Compose a [`NatsSubjectChannel`] pinned to the in-cluster
809 /// default NATS URL ([`DEFAULT_NATS_URL`], stamped as `url: None`
810 /// so [`Self::resolved_url`] resolves the same URL on read) and
811 /// the given `subject` + `stream` — the ONE substrate primitive
812 /// owning the 3-slot `NatsSubjectChannel { subject, stream, url:
813 /// None }` fixture literal every consumer restated by hand
814 /// pre-lift.
815 ///
816 /// Pre-lift the same 3-slot chain (`subject: <s>.into()`,
817 /// `stream: <s>.into()`, `url: None`) was hand-authored at SIX
818 /// workspace-wide sites past the ★★ PRIME-DIRECTIVE ≥ 2
819 /// duplication threshold, EVERY one of them the "default-URL,
820 /// subject-and-stream-only" shape (no site in the workspace
821 /// stamps a non-None `url` on a `NatsSubjectChannel` literal):
822 ///
823 /// * `tatara-process::export` — two sites: the
824 /// `vector_channel_resolves_nats_subject` round-trip fixture
825 /// plus the `single_slot_channel` closed-set ambiguity probe.
826 /// * `tatara-process::tagged_union` — one site: the
827 /// `single_slot_vector_channel_probe` closed-set probe (the
828 /// sibling of the `single_slot_channel` fixture in
829 /// `tatara-process::export`).
830 /// * `tatara-reconciler::render` — one site: the
831 /// `spec_receipts_attested` fixture the ephemeral-export-Job
832 /// renderer's tests seed.
833 /// * `tatara-export-worker::lib` — two sites: the
834 /// `subject_substitutes_run_id_template` template-substitution
835 /// fixture plus the `subject_passthrough_when_no_template`
836 /// passthrough fixture.
837 ///
838 /// Post-lift every callsite reads
839 /// `NatsSubjectChannel::publish(<subject>, <stream>)` and the
840 /// three-slot struct's `url` slot stays owned by the ONE
841 /// substrate site. The `impl Into<String>` bound on both
842 /// positional args accepts every pre-lift caller shape verbatim
843 /// — `&'static str` literals (`"S"`, `"EPHEMERAL_RECEIPTS"`,
844 /// `"pleme.pleme-dev.ephemeral.{{run_id}}.receipt"`), owned
845 /// `String` values, and `.into()`-terminated chains alike —
846 /// without a per-site coercion.
847 ///
848 /// A future addition (a default-URL override for a per-fleet
849 /// NATS endpoint, a per-fleet `subject` prefix normalization, a
850 /// `stream` clamp against a shinryu-registered stream catalog,
851 /// or an authenticated NATS URL seed pulled from a config
852 /// surface) lands at THIS ONE substrate primitive and every
853 /// downstream consumer inherits the upgrade mechanically — no
854 /// per-site edit at any of the six listed callers or at future
855 /// test fixtures.
856 ///
857 /// Theory anchor: THEORY.md §VI.1 (generation over composition —
858 /// the `NatsSubjectChannel { subject, stream, url: None }`
859 /// fixture literal recurred at six hand-authored sites past the
860 /// ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
861 /// ONE owner here). THEORY.md §II.1 invariant 5 (composition
862 /// preserves proofs — a regression that drifted the default-URL
863 /// sentinel from `None` to a hardcoded string, or reordered the
864 /// three struct slots, surfaces at the
865 /// `publish_composes_byte_identical_to_pre_lift_literal_across_every_subject_stream_pair`
866 /// pin below rather than as silent skew at every downstream
867 /// fixture).
868 #[must_use]
869 pub fn publish(subject: impl Into<String>, stream: impl Into<String>) -> Self {
870 Self {
871 subject: subject.into(),
872 stream: stream.into(),
873 url: None,
874 }
875 }
876}
877
878/// Stdout channel — worker prints the event; Vector picks up via
879/// `kubernetes_logs`.
880#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
881#[serde(rename_all = "camelCase")]
882pub struct StdoutChannel {
883 /// Pretty-print JSON (multi-line) instead of compact NDJSON.
884 /// Defaults to false — compact NDJSON matches Vector's parser.
885 #[serde(default)]
886 pub pretty: bool,
887}
888
889// ─── ExportTrigger ─────────────────────────────────────────────────
890
891/// When the export fires. Aligns with `ProcessPhase` so the
892/// reconciler's `Releasing` phase can match against the terminal
893/// phase reached directly.
894#[derive(
895 Clone,
896 Copy,
897 Debug,
898 PartialEq,
899 Eq,
900 Hash,
901 Serialize,
902 Deserialize,
903 JsonSchema,
904 Default,
905 tatara_closed_set::DeriveClosedSet,
906)]
907#[serde(rename_all = "PascalCase")]
908#[closed_set(via = "as_str", generate_unknown)]
909pub enum ExportTrigger {
910 /// Fire when the Process reaches `Attested`. Default — matches
911 /// the most common case (capture successful-run artifacts).
912 #[default]
913 OnAttested,
914 /// Fire when the Process reaches `Failed`. Use for failure
915 /// post-mortems (process snapshots, last receipts).
916 OnFailed,
917 /// Fire on every terminal phase (`Attested` or `Failed`). Use
918 /// for run markers that need to surface regardless of outcome.
919 Always,
920}
921
922impl ExportTrigger {
923 /// The closed set of export triggers — single source of truth that
924 /// drives the `as_str` / Display / `FromStr` triad and the typed
925 /// `fires_on` dispatch over `ProcessPhase`. Adding a fourth variant
926 /// lands at one `ALL` entry + one `as_str` arm + one `fires_on` arm
927 /// — exhaustively checked by the compiler (the `[Self; 3]` array
928 /// literal forces the arity).
929 ///
930 /// Sibling closed-set lifts on the same `ProcessSpec` axis:
931 /// [`crate::lifetime::TeardownPolicy::ALL`],
932 /// [`crate::intent::IntentKind::ALL`],
933 /// [`crate::lifetime::LifetimeKind::ALL`],
934 /// [`crate::boundary::ConditionKind::ALL`],
935 /// [`crate::phase::ProcessPhase::ALL`],
936 /// [`crate::signal::ProcessSignal::ALL`].
937 pub const ALL: [Self; 3] = [Self::OnAttested, Self::OnFailed, Self::Always];
938
939 /// Canonical PascalCase wire-format projection — matches the serde
940 /// `rename_all = "PascalCase"` output verbatim. Used by Display
941 /// (single source of truth), by `FromStr` to identify the variant
942 /// from its annotation / status-field representation, and by
943 /// operator-facing reason strings without reaching for `{:?}` Debug
944 /// formatting. Pinned by `export_trigger_as_str_matches_serde`.
945 pub const fn as_str(self) -> &'static str {
946 match self {
947 Self::OnAttested => "OnAttested",
948 Self::OnFailed => "OnFailed",
949 Self::Always => "Always",
950 }
951 }
952
953 /// True iff, given a `ProcessPhase`, this trigger says "fire."
954 /// ONE typed dispatch over the typed phase enum that replaces the
955 /// four hand-rolled `match phase { Attested => fires_on_attested(),
956 /// Failed => fires_on_failed(), _ => false }` sites the reconciler
957 /// and `EphemeralLifetime` previously branched on. Every
958 /// non-terminal phase always returns `false` — exports are a
959 /// terminal-phase decision, now enforced by the closed-set match
960 /// over `ProcessPhase`.
961 ///
962 /// The legacy [`Self::fires_on_attested`] / [`Self::fires_on_failed`]
963 /// predicates remain as thin delegates so existing call sites keep
964 /// their narrow signatures; the truth table is pinned by
965 /// `export_trigger_legacy_predicates_delegate_to_phase_dispatch`.
966 pub const fn fires_on(self, phase: ProcessPhase) -> bool {
967 match phase {
968 ProcessPhase::Attested => matches!(self, Self::OnAttested | Self::Always),
969 ProcessPhase::Failed => matches!(self, Self::OnFailed | Self::Always),
970 ProcessPhase::Pending
971 | ProcessPhase::Forking
972 | ProcessPhase::Execing
973 | ProcessPhase::Running
974 | ProcessPhase::Reconverging
975 | ProcessPhase::Releasing
976 | ProcessPhase::Exiting
977 | ProcessPhase::Zombie
978 | ProcessPhase::Reaped => false,
979 }
980 }
981
982 /// Thin delegate to [`Self::fires_on`] for the `Attested` case —
983 /// kept so existing call sites that already know the gate keep
984 /// their narrow signature without reaching for the typed-phase
985 /// variant.
986 pub const fn fires_on_attested(self) -> bool {
987 self.fires_on(ProcessPhase::Attested)
988 }
989
990 /// Symmetric delegate to [`Self::fires_on`] for the `Failed` case.
991 pub const fn fires_on_failed(self) -> bool {
992 self.fires_on(ProcessPhase::Failed)
993 }
994}
995
996impl fmt::Display for ExportTrigger {
997 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
998 f.write_str(self.as_str())
999 }
1000}
1001
1002// `impl FromStr for ExportTrigger` + `impl tatara_lisp::ClosedSet for
1003// ExportTrigger` are generated by `#[derive(tatara_closed_set::DeriveClosedSet)]`
1004// on the enum declaration above.
1005
1006// `pub struct UnknownExportTrigger(pub String)` is generated by
1007// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
1008// on the enum declaration above. The auto-derived label `"export trigger"`
1009// matches the prior hand-rolled `#[error("unknown export trigger: {0}")]`
1010// verbatim. Symmetric to [`UnknownChannelKind`],
1011// [`UnknownReportFormat`], [`crate::lifetime::UnknownTeardownPolicy`],
1012// [`crate::boundary::UnknownConditionKind`], and
1013// [`crate::phase::UnknownPhase`].
1014
1015/// Slice-level `(ExportTrigger, presence)` probe on any `&[ExportSpec]`
1016/// — the ONE substrate primitive that owns the
1017/// `.iter().any(|e| e.when == K)` walk shape for the export-spec
1018/// vector. Callers compose the answer they want on top:
1019/// `spec.lifetime.resolved_ephemeral().is_some_and(|e|
1020/// e.exports.has_when(kind))` for the point-domain
1021/// `export-when-<kind>` require-tag family, a coherence check that
1022/// verifies "every `Always` export ships through a JetStream-backed
1023/// channel", an editor completion listing which
1024/// [`ExportTrigger`] gates the operator authored — every future
1025/// consumer reaches this ONE primitive through
1026/// `slice.has_when(k)` instead of restating the `.iter().any`
1027/// closure body.
1028///
1029/// # Fourth instance in the slice-level presence-probe algebra
1030///
1031/// Same axis, same shape, fourth instance in the workspace-wide
1032/// slice-level closed-set-driven presence-probe algebra alongside
1033/// [`crate::boundary::ConditionSliceExt::has_kind`] on `&[Condition]`,
1034/// [`crate::spec::DependsOnSliceExt::has_must_reach`] on
1035/// `&[DependsOn]`, and
1036/// [`crate::compliance::ComplianceBindingSliceExt::has_verification_phase`]
1037/// on `&[ComplianceBinding]`. All four live one composition boundary
1038/// below the tagged-union-parent probes ([`crate::intent::Intent::has`],
1039/// [`crate::lifetime::Lifetime::has`],
1040/// [`crate::boundary::Boundary::has_condition_kind`]) at the
1041/// (`&self`, `K`) → `bool` signature; a future normalization at the
1042/// slice-level probe shape (widening the return to
1043/// `Option<&ExportSpec>` for deeper diagnostics, adding a debug-build
1044/// assertion on redundant duplicate `(source, channel)` pairs at the
1045/// same trigger, switching to a linear scan that also counts matches)
1046/// lands at ONE site here and every downstream
1047/// `slice.has_when(K)` callsite picks it up mechanically.
1048///
1049/// # Compounding
1050///
1051/// The `export-when-<kind>` and `channel-<kind>` require-tag prefix
1052/// families in `tatara-reconciler::bin::tatara-check` compose the
1053/// [`Self::has_when`] and [`Self::has_channel_kind`] primitives with
1054/// the closed-set `FromStr` autoderived on [`ExportTrigger`] +
1055/// [`ChannelKind`] through the `strip_and_classify_prefixed_kind`
1056/// substrate to publish the SEVENTH + EIGHTH closed-set-driven prefix
1057/// families byte-for-byte symmetrical with `intent-<kind>` /
1058/// `lifetime-<kind>` / `condition-<kind>` / `must-reach-<kind>` /
1059/// `sighup-<kind>` / `verification-phase-<kind>`. The
1060/// `resolved_ephemeral` projection on the parent
1061/// [`crate::lifetime::Lifetime`] gates BOTH walks: a permanent
1062/// Process (or an ambiguous one, or one without an `:exports` slot)
1063/// returns `false` for every trigger kind AND every channel kind
1064/// because the exports vector isn't reachable — the same operator-
1065/// facing answer as a resolved-ephemeral spec whose `exports` slot is
1066/// present but empty. A future fourth [`ExportTrigger`] variant added
1067/// to [`ExportTrigger::ALL`] (a hypothetical `OnFirstFailure` retry-
1068/// scoped trigger, an `OnAborted` cancellation-scoped trigger)
1069/// reaches every downstream through the SAME closed-set walk with no
1070/// per-caller edit, as does a future fourth [`ChannelKind`] variant
1071/// added to [`ChannelKind::ALL`] (a hypothetical
1072/// `KubernetesEventSink` slot, a `WebhookPost` slot).
1073///
1074/// Peer to [`crate::lifetime::EphemeralLifetime::has_applicable_exports`]
1075/// on the `(ExportTrigger, ProcessPhase) → bool` axis pair — that
1076/// projection asks "does any export FIRE at this terminal phase" (a
1077/// compound `(when, phase) → fires_on(phase)` walk that projects to
1078/// a boolean the reconciler consumes in the `Releasing` gate); this
1079/// primitive asks "does any export CARRY this trigger literal" (a
1080/// direct `when == kind` equality walk that answers the operator's
1081/// `:requires (export-when-<kind>)` audit tag). The two surfaces
1082/// answer distinct questions and coexist — `has_applicable_exports`
1083/// composes the closed-set dispatch over `ExportTrigger::fires_on`,
1084/// while `has_when` composes the closed-set discriminator equality
1085/// on the raw `when` field.
1086///
1087/// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
1088/// proofs; the per-slice `when` walk lives at ONE substrate site so
1089/// every downstream (require-tag classifier, coherence check, editor
1090/// completion) binds through the SAME shape rather than restating the
1091/// `.iter().any(|e| e.when == K)` closure body at each callsite.
1092/// THEORY.md §VI.1 — generation over composition; a future
1093/// [`ExportTrigger`] variant lands at ONE `ALL` entry + ONE `as_str`
1094/// arm on the closed set and the presence probe picks it up
1095/// mechanically without further per-consumer edits.
1096pub trait ExportSpecSliceExt {
1097 /// True iff at least one [`ExportSpec`] in this slice carries the
1098 /// given [`ExportTrigger`] on its [`ExportSpec::when`] slot. The
1099 /// single-slice presence probe every consumer of the
1100 /// `(&[ExportSpec], ExportTrigger) -> bool` shape composes against.
1101 fn has_when(&self, kind: ExportTrigger) -> bool;
1102
1103 /// True iff at least one [`ExportSpec`] in this slice ships through
1104 /// a [`VectorChannel`] whose `ChannelKind::select`-populated slot
1105 /// matches the given [`ChannelKind`]. Composes the closed-set
1106 /// [`ChannelKind::select`] projection with the same
1107 /// `.iter().any(|e| …)` walk shape [`Self::has_when`] publishes on
1108 /// the `when` axis, opening a second closed-set-driven presence
1109 /// probe on the SAME `&[ExportSpec]` slice. The
1110 /// [`ChannelKind::select`] projection is the ONE substrate owner
1111 /// of the "is the slot populated" answer for a tagged-union
1112 /// carrier, so a future fourth [`ChannelKind`] variant reaches this
1113 /// walk through the `ALL` sweep + one `select` arm alone — no
1114 /// per-consumer edit here.
1115 ///
1116 /// Note that an ambiguous [`VectorChannel`] (two or more slots
1117 /// populated — a schema-invalid state that
1118 /// [`VectorChannel::variant`] rejects) STILL answers `true` for
1119 /// every kind whose slot is populated because the walk reads the
1120 /// raw `Option<…>` slot rather than the validated tagged-union
1121 /// resolver. Callers whose invariant is "the spec is valid" reach
1122 /// the same answer through the validation gate rather than this
1123 /// primitive; callers whose invariant is "is this kind's slot
1124 /// present at all" (require-tag classifier, editor completion,
1125 /// coherence check that flags a `stdout` export as
1126 /// non-guaranteed-delivery) reach through THIS primitive.
1127 fn has_channel_kind(&self, kind: ChannelKind) -> bool;
1128
1129 /// True iff at least one [`ExportSpec`] in this slice carries an
1130 /// [`ArtifactSource::test_report`] populated with a
1131 /// [`TestReportSource`] whose [`TestReportSource::format`] equals
1132 /// the given [`ReportFormat`]. Composes the nested-`Option`
1133 /// projection past `source.test_report` with the same
1134 /// `.iter().any(|e| …)` walk shape [`Self::has_when`] and
1135 /// [`Self::has_channel_kind`] publish, opening a THIRD closed-set-
1136 /// driven presence probe on the SAME `&[ExportSpec]` slice — the
1137 /// first probe on this trait that reads a NESTED-Option scalar
1138 /// (`Option<TestReportSource>` past `ArtifactSource`, then
1139 /// `ReportFormat` equality on the populated slot) rather than a
1140 /// tagged-union carrier (`has_channel_kind` on `VectorChannel`) or
1141 /// a direct closed-set field (`has_when` on `ExportTrigger`). A
1142 /// future fifth [`ReportFormat`] variant reaches this walk through
1143 /// the [`ReportFormat::ALL`] sweep alone — the equality body reads
1144 /// the raw `format` field so no per-variant substrate edit lands
1145 /// here.
1146 ///
1147 /// Note that an export whose `source` has NO `test_report` slot
1148 /// populated (a `receipts` / `process_snapshot` / `run_marker`
1149 /// export) contributes `false` for EVERY [`ReportFormat`] kind —
1150 /// the outer nested-Option projection collapses before the
1151 /// equality on `format` fires, so the default [`ReportFormat::Raw`]
1152 /// (which the empty projection would spuriously compare to a
1153 /// hand-authored `Raw`-tagged report) never leaks through.
1154 /// Callers whose invariant is "does any test-report export declare
1155 /// THIS payload format" (require-tag classifier, coherence check
1156 /// like "every JUnit report ships through JetStream", editor
1157 /// completion) reach this ONE primitive rather than restating the
1158 /// `e.source.test_report.as_ref().is_some_and(|tr| tr.format == K)`
1159 /// two-step chain at each callsite.
1160 fn has_report_format(&self, kind: ReportFormat) -> bool;
1161
1162 /// True iff at least one [`ExportSpec`] in this slice carries an
1163 /// [`ArtifactSource`] whose `ArtifactKind::select`-populated slot
1164 /// matches the given [`ArtifactKind`]. Composes the closed-set
1165 /// [`ArtifactKind::select`] projection with the same
1166 /// `.iter().any(|e| …)` walk shape [`Self::has_when`] +
1167 /// [`Self::has_channel_kind`] + [`Self::has_report_format`] publish,
1168 /// opening a FOURTH closed-set-driven presence probe on the SAME
1169 /// `&[ExportSpec]` slice — the second probe on this trait whose
1170 /// closure reads a TAGGED-UNION carrier (peer of
1171 /// [`Self::has_channel_kind`] on `VectorChannel`; the
1172 /// [`ArtifactKind::select`] projection is the ONE substrate owner
1173 /// of the "is the slot populated" answer for the four-slot
1174 /// [`ArtifactSource`] carrier, so a future fifth [`ArtifactKind`]
1175 /// variant reaches this walk through the [`ArtifactKind::ALL`]
1176 /// sweep + one `select` arm alone — no per-consumer edit here.
1177 ///
1178 /// Note that an ambiguous [`ArtifactSource`] (two or more slots
1179 /// populated — a schema-invalid state that
1180 /// [`ArtifactSource::variant`] rejects) STILL answers `true` for
1181 /// every kind whose slot is populated because the walk reads the
1182 /// raw `Option<…>` slot rather than the validated tagged-union
1183 /// resolver. Callers whose invariant is "the spec is valid" reach
1184 /// the same answer through the validation gate rather than this
1185 /// primitive; callers whose invariant is "is this kind's slot
1186 /// present at all" (require-tag classifier, editor completion,
1187 /// coherence check like "every ephemeral export ships a receipts
1188 /// artifact") reach through THIS primitive.
1189 ///
1190 /// Distinct from [`Self::has_report_format`] on ONE dimension:
1191 /// `has_report_format` reads a NESTED-Option scalar
1192 /// (`Option<TestReportSource>` past `ArtifactSource`, then
1193 /// `ReportFormat` equality on the populated slot) while
1194 /// `has_artifact_kind` reads the OUTER tagged-union carrier
1195 /// directly (`Option<T>` past `ArtifactSource`) — a receipts-only
1196 /// export answers `true` for `has_artifact_kind(Receipts)` but
1197 /// `false` for every `has_report_format(k)` kind because the
1198 /// `test_report` slot is empty.
1199 fn has_artifact_kind(&self, kind: ArtifactKind) -> bool;
1200}
1201
1202impl ExportSpecSliceExt for [ExportSpec] {
1203 fn has_when(&self, kind: ExportTrigger) -> bool {
1204 self.iter().any(|e| e.when == kind)
1205 }
1206
1207 fn has_channel_kind(&self, kind: ChannelKind) -> bool {
1208 self.iter().any(|e| kind.select(&e.channel).is_some())
1209 }
1210
1211 fn has_report_format(&self, kind: ReportFormat) -> bool {
1212 self.iter().any(|e| {
1213 e.source
1214 .test_report
1215 .as_ref()
1216 .is_some_and(|tr| tr.format == kind)
1217 })
1218 }
1219
1220 fn has_artifact_kind(&self, kind: ArtifactKind) -> bool {
1221 self.iter().any(|e| kind.select(&e.source).is_some())
1222 }
1223}
1224
1225// ─── Tests ─────────────────────────────────────────────────────────
1226
1227#[cfg(test)]
1228mod tests {
1229 use super::*;
1230
1231 #[test]
1232 fn artifact_source_empty_errors() {
1233 let s = ArtifactSource::default();
1234 match s.variant().unwrap_err() {
1235 ArtifactError::Empty(list) => assert_eq!(list, ARTIFACT_KIND_LIST),
1236 other => panic!("expected Empty, got {other:?}"),
1237 }
1238 }
1239
1240 #[test]
1241 fn artifact_source_receipts_resolves() {
1242 let s = ArtifactSource {
1243 receipts: Some(ReceiptsSource::default()),
1244 ..ArtifactSource::default()
1245 };
1246 assert!(matches!(s.variant().unwrap(), ArtifactVariant::Receipts(_)));
1247 }
1248
1249 #[test]
1250 fn artifact_source_two_variants_ambiguous() {
1251 let s = ArtifactSource {
1252 receipts: Some(ReceiptsSource::default()),
1253 test_report: Some(TestReportSource {
1254 configmap: "x".into(),
1255 key: "y".into(),
1256 format: ReportFormat::Junit,
1257 namespace: None,
1258 }),
1259 ..ArtifactSource::default()
1260 };
1261 assert_eq!(s.variant().unwrap_err(), ArtifactError::Ambiguous);
1262 }
1263
1264 #[test]
1265 fn vector_channel_empty_errors() {
1266 let c = VectorChannel::default();
1267 match c.variant().unwrap_err() {
1268 ChannelError::Empty(list) => assert_eq!(list, CHANNEL_KIND_LIST),
1269 other => panic!("expected Empty, got {other:?}"),
1270 }
1271 }
1272
1273 #[test]
1274 fn vector_channel_resolves_http_event() {
1275 let c = VectorChannel {
1276 http_event: Some(HttpEventChannel::signal("test-report")),
1277 ..VectorChannel::default()
1278 };
1279 match c.variant().unwrap() {
1280 ChannelVariant::HttpEvent(h) => {
1281 assert_eq!(h.signal_type, "test-report");
1282 assert_eq!(h.resolved_endpoint(), DEFAULT_VECTOR_INGEST);
1283 }
1284 other => panic!("expected HttpEvent, got {other:?}"),
1285 }
1286 }
1287
1288 #[test]
1289 fn vector_channel_resolves_nats_subject() {
1290 let c = VectorChannel {
1291 nats_subject: Some(NatsSubjectChannel::publish(
1292 "pleme.pleme-dev.ephemeral.{{run_id}}.receipt",
1293 "EPHEMERAL_RECEIPTS",
1294 )),
1295 ..VectorChannel::default()
1296 };
1297 match c.variant().unwrap() {
1298 ChannelVariant::NatsSubject(n) => {
1299 assert_eq!(n.stream, "EPHEMERAL_RECEIPTS");
1300 assert_eq!(n.resolved_url(), DEFAULT_NATS_URL);
1301 }
1302 other => panic!("expected NatsSubject, got {other:?}"),
1303 }
1304 }
1305
1306 #[test]
1307 fn export_trigger_fire_logic() {
1308 assert!(ExportTrigger::OnAttested.fires_on_attested());
1309 assert!(!ExportTrigger::OnAttested.fires_on_failed());
1310 assert!(ExportTrigger::OnFailed.fires_on_failed());
1311 assert!(!ExportTrigger::OnFailed.fires_on_attested());
1312 assert!(ExportTrigger::Always.fires_on_attested());
1313 assert!(ExportTrigger::Always.fires_on_failed());
1314 }
1315
1316 // ── closed-set algebra for ExportTrigger (ALL × as_str × FromStr ×
1317 // fires_on(phase)) ─
1318
1319 /// `ALL` is the source of truth for the resolver / `FromStr` sweep
1320 /// Structural well-formedness of [`ExportTrigger`] as a
1321 /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1322 /// testkit lift that pins all three structural invariants (`ALL`
1323 /// is non-empty, every variant round-trips through
1324 /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1325 /// outside the closed set) at ONE call site. Replaces the hand-
1326 /// derived `export_trigger_all_is_unique_and_complete` +
1327 /// `export_trigger_roundtrip_via_as_str` + the empty-input arm of
1328 /// `unknown_export_trigger_errors`. `FromStr` delegates to
1329 /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1330 /// exercises the same code path the reconciler hits when parsing
1331 /// a CRD `enum:`-validated value back to the typed trigger.
1332 #[test]
1333 fn export_trigger_is_well_formed_closed_set() {
1334 tatara_closed_set::assert_closed_set_well_formed::<ExportTrigger>();
1335 }
1336
1337 /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1338 /// output verbatim for every variant. A future variant rename
1339 /// (or an `as_str` arm typo) lands here at one site, instead of
1340 /// drifting between the typed surface and the YAML wire format
1341 /// the reconciler / operator both read.
1342 #[test]
1343 fn export_trigger_as_str_matches_serde() {
1344 crate::tagged_union::assert_label_matches_serde_serialization::<ExportTrigger>();
1345 }
1346
1347 /// The Display impl IS `as_str` — pinning this lets future callers
1348 /// reach for either projection without drift. If a reviewer
1349 /// accidentally re-introduces an inline match in Display, this
1350 /// test would fail the moment a variant rename touches one site
1351 /// but not the other.
1352 #[test]
1353 fn export_trigger_display_matches_as_str() {
1354 crate::tagged_union::assert_display_matches_label::<ExportTrigger>();
1355 }
1356
1357 /// `FromStr` rejects strings that aren't in the canonical
1358 /// projection — lowercased / typo / unrelated — and the error
1359 /// echoes the input verbatim so the operator-facing diagnostic
1360 /// carries the offending value, not a normalized form. The
1361 /// empty-input arm is pinned by
1362 /// [`export_trigger_is_well_formed_closed_set`] via the
1363 /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1364 /// verbatim-echo contract on the [`UnknownExportTrigger`] newtype,
1365 /// which the trait's `make_unknown` can't see.
1366 #[test]
1367 fn unknown_export_trigger_errors() {
1368 use std::str::FromStr;
1369 for bad in ["onAttested", "ALWAYS", "Never", "OnSuccess"] {
1370 let err = ExportTrigger::from_str(bad).unwrap_err();
1371 assert_eq!(err.0, bad, "error payload should echo input verbatim");
1372 }
1373 }
1374
1375 // `unknown_export_trigger_message_matches_substrate_convention`
1376 // removed — clause (5) of
1377 // `tatara_closed_set::assert_closed_set_well_formed::<ExportTrigger>()`
1378 // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
1379 // shape generically (called from `trigger_is_well_formed_closed_set`
1380 // above); the `SET_LABEL` projection is pinned by
1381 // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
1382
1383 /// TRUTH-TABLE CONTRACT: `fires_on(phase)` agrees with the
1384 /// documented (trigger, phase) -> bool table for every (3 × 11)
1385 /// combination. A new variant in either `ExportTrigger` or
1386 /// `ProcessPhase` reaches this test by iteration — adding a phase
1387 /// without extending `fires_on`'s match would be caught by the
1388 /// compiler (the closed-set match over `ProcessPhase` enforces it);
1389 /// adding a trigger without extending its truth row is caught
1390 /// here.
1391 #[test]
1392 fn export_trigger_fires_on_truth_table() {
1393 // ProcessPhase imports are local to the test to keep the
1394 // module's top-level surface minimal.
1395 use crate::phase::ProcessPhase::{
1396 Attested, Execing, Exiting, Failed, Forking, Pending, Reaped, Reconverging, Releasing,
1397 Running, Zombie,
1398 };
1399 let table: &[(ExportTrigger, &[(crate::phase::ProcessPhase, bool)])] = &[
1400 (
1401 ExportTrigger::OnAttested,
1402 &[
1403 (Attested, true),
1404 (Failed, false),
1405 (Pending, false),
1406 (Forking, false),
1407 (Execing, false),
1408 (Running, false),
1409 (Reconverging, false),
1410 (Releasing, false),
1411 (Exiting, false),
1412 (Zombie, false),
1413 (Reaped, false),
1414 ],
1415 ),
1416 (
1417 ExportTrigger::OnFailed,
1418 &[
1419 (Attested, false),
1420 (Failed, true),
1421 (Pending, false),
1422 (Forking, false),
1423 (Execing, false),
1424 (Running, false),
1425 (Reconverging, false),
1426 (Releasing, false),
1427 (Exiting, false),
1428 (Zombie, false),
1429 (Reaped, false),
1430 ],
1431 ),
1432 (
1433 ExportTrigger::Always,
1434 &[
1435 (Attested, true),
1436 (Failed, true),
1437 (Pending, false),
1438 (Forking, false),
1439 (Execing, false),
1440 (Running, false),
1441 (Reconverging, false),
1442 (Releasing, false),
1443 (Exiting, false),
1444 (Zombie, false),
1445 (Reaped, false),
1446 ],
1447 ),
1448 ];
1449 // The truth table must cover every (trigger, phase) pair.
1450 assert_eq!(table.len(), ExportTrigger::ALL.len());
1451 for (_, row) in table {
1452 assert_eq!(row.len(), crate::phase::ProcessPhase::ALL.len());
1453 }
1454 for (trigger, row) in table {
1455 for (phase, expected) in *row {
1456 assert_eq!(
1457 trigger.fires_on(*phase),
1458 *expected,
1459 "fires_on({trigger:?}, {phase:?}) drift"
1460 );
1461 }
1462 }
1463 }
1464
1465 /// DELEGATION CONTRACT: the legacy `fires_on_attested` /
1466 /// `fires_on_failed` predicates agree with the typed
1467 /// `fires_on(phase)` dispatch they delegate to, for every variant
1468 /// in `ALL`. A regression that re-introduces an inline `matches!`
1469 /// in either legacy predicate fails here. `fires_on` is the
1470 /// source of truth.
1471 #[test]
1472 fn export_trigger_legacy_predicates_delegate_to_phase_dispatch() {
1473 for trigger in ExportTrigger::ALL {
1474 assert_eq!(
1475 trigger.fires_on_attested(),
1476 trigger.fires_on(crate::phase::ProcessPhase::Attested),
1477 "legacy fires_on_attested drift for {trigger:?}"
1478 );
1479 assert_eq!(
1480 trigger.fires_on_failed(),
1481 trigger.fires_on(crate::phase::ProcessPhase::Failed),
1482 "legacy fires_on_failed drift for {trigger:?}"
1483 );
1484 }
1485 }
1486
1487 // ── closed-set algebra for ReportFormat (ALL × as_str × FromStr ×
1488 // payload_shape) ─
1489
1490 /// Structural well-formedness of [`ReportFormat`] as a
1491 /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1492 /// testkit lift that pins all three structural invariants (`ALL`
1493 /// is non-empty, every variant round-trips through
1494 /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1495 /// outside the closed set) at ONE call site. Replaces the hand-
1496 /// derived `report_format_all_is_unique_and_complete` +
1497 /// `report_format_roundtrip_via_as_str` + the empty-input arm of
1498 /// `unknown_report_format_errors`. `FromStr` delegates to
1499 /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1500 /// exercises the same code path the export worker hits when
1501 /// parsing a CRD `enum:`-validated value back to the typed format.
1502 #[test]
1503 fn report_format_is_well_formed_closed_set() {
1504 tatara_closed_set::assert_closed_set_well_formed::<ReportFormat>();
1505 }
1506
1507 /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1508 /// output verbatim for every variant. A future variant rename
1509 /// (or an `as_str` arm typo) lands here at one site, instead of
1510 /// drifting between the typed surface and the YAML wire format
1511 /// the reconciler / operator both read.
1512 #[test]
1513 fn report_format_as_str_matches_serde() {
1514 crate::tagged_union::assert_label_matches_serde_serialization::<ReportFormat>();
1515 }
1516
1517 /// The Display impl IS `as_str` — pinning this lets future callers
1518 /// reach for either projection without drift.
1519 #[test]
1520 fn report_format_display_matches_as_str() {
1521 crate::tagged_union::assert_display_matches_label::<ReportFormat>();
1522 }
1523
1524 /// `FromStr` rejects strings that aren't in the canonical
1525 /// projection — lowercased / typo / unrelated — and the error
1526 /// echoes the input verbatim so the operator-facing diagnostic
1527 /// carries the offending value, not a normalized form. The
1528 /// empty-input arm is pinned by
1529 /// [`report_format_is_well_formed_closed_set`] via the
1530 /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1531 /// verbatim-echo contract on the [`UnknownReportFormat`] newtype,
1532 /// which the trait's `make_unknown` can't see.
1533 #[test]
1534 fn unknown_report_format_errors() {
1535 use std::str::FromStr;
1536 for bad in ["junit", "JUNIT", "tap", "Yaml", "TomlV1"] {
1537 let err = ReportFormat::from_str(bad).unwrap_err();
1538 assert_eq!(err.0, bad, "error payload should echo input verbatim");
1539 }
1540 }
1541
1542 // `unknown_report_format_message_matches_substrate_convention`
1543 // removed — clause (5) of
1544 // `tatara_closed_set::assert_closed_set_well_formed::<ReportFormat>()`
1545 // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
1546 // shape generically (called from `report_format_is_well_formed_closed_set`
1547 // above); the `SET_LABEL` projection is pinned by
1548 // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
1549
1550 /// TRUTH-TABLE CONTRACT: `payload_shape` agrees with the documented
1551 /// shape table for every variant in `ALL`. A new variant whose
1552 /// shape the author forgets to add to `payload_shape`'s match is
1553 /// caught by the compiler at the match site; a regression that
1554 /// reshuffles existing variants (e.g. routing `NdJson` to opaque
1555 /// bytes) is caught here. `payload_shape` is the worker's only
1556 /// dispatch — once this passes, the worker's `match shape { … }`
1557 /// is exhaustive on the 2-variant `ReportPayloadShape` instead of
1558 /// the 4-variant `ReportFormat`, so adding a future format never
1559 /// touches the worker.
1560 #[test]
1561 fn report_format_payload_shape_truth_table() {
1562 let table: &[(ReportFormat, ReportPayloadShape)] = &[
1563 (ReportFormat::Junit, ReportPayloadShape::OpaqueBytes),
1564 (ReportFormat::TapV13, ReportPayloadShape::OpaqueBytes),
1565 (ReportFormat::NdJson, ReportPayloadShape::NdJsonLines),
1566 (ReportFormat::Raw, ReportPayloadShape::OpaqueBytes),
1567 ];
1568 assert_eq!(table.len(), ReportFormat::ALL.len());
1569 for (format, expected) in table {
1570 assert_eq!(
1571 format.payload_shape(),
1572 *expected,
1573 "payload_shape({format:?}) drift"
1574 );
1575 }
1576 }
1577
1578 /// CLOSURE-OF-PROJECTION CONTRACT: every `ReportPayloadShape`
1579 /// variant is the image of at least one `ReportFormat` variant —
1580 /// no shape is stranded. A `Compressed` shape added to
1581 /// `ReportPayloadShape::ALL` without an `ALL → payload_shape`
1582 /// mapping at any `ReportFormat` arm makes the worker's
1583 /// 3-variant dispatch reachable from no input, which would
1584 /// silently dead-code one arm. Caught here.
1585 #[test]
1586 fn report_payload_shape_reachable_from_some_report_format() {
1587 for shape in ReportPayloadShape::ALL {
1588 let reachable = ReportFormat::ALL.iter().any(|f| f.payload_shape() == shape);
1589 assert!(
1590 reachable,
1591 "{shape:?} is in ReportPayloadShape::ALL but no ReportFormat projects to it"
1592 );
1593 }
1594 }
1595
1596 /// CLOSED-SET CONTRACT: `ReportPayloadShape::ALL` enumerates each
1597 /// variant exactly once. The `[Self; 2]` array literal forces
1598 /// the arity at compile time; this test pins per-variant
1599 /// reachability so adding a third shape (`Compressed`) without
1600 /// extending `ALL` fails here rather than silently dropping the
1601 /// new variant from every sweep through `Self::ALL`.
1602 #[test]
1603 fn report_payload_shape_all_enumerates_each_variant_exactly_once() {
1604 let mut seen = std::collections::HashSet::new();
1605 for shape in ReportPayloadShape::ALL {
1606 assert!(seen.insert(shape), "duplicate variant in ALL: {shape:?}");
1607 }
1608 assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
1609 for shape in [
1610 ReportPayloadShape::NdJsonLines,
1611 ReportPayloadShape::OpaqueBytes,
1612 ] {
1613 assert!(
1614 ReportPayloadShape::ALL.contains(&shape),
1615 "{shape:?} declared but not in ALL"
1616 );
1617 }
1618 }
1619
1620 /// CANONICAL-KEY UNIQUENESS: no two shapes alias the same
1621 /// `as_str` identifier. A future rename of one variant to a name
1622 /// that collides with another (e.g. both → `"Lines"`) breaks the
1623 /// shape's identity in operator-facing reason strings and would
1624 /// silently make Display non-injective. Caught here.
1625 #[test]
1626 fn report_payload_shape_as_str_unique_per_variant() {
1627 let mut seen = std::collections::HashSet::new();
1628 for shape in ReportPayloadShape::ALL {
1629 assert!(
1630 seen.insert(shape.as_str()),
1631 "as_str collision: {shape:?} → {:?}",
1632 shape.as_str()
1633 );
1634 }
1635 assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
1636 }
1637
1638 /// DISPLAY-IS-AS_STR: the Display impl IS `as_str` — pinning
1639 /// this lets callers reach for either projection without drift.
1640 /// Sibling to `report_format_display_matches_as_str` and
1641 /// `export_trigger_display_matches_as_str`. Routed through the
1642 /// substrate-wide [`crate::tagged_union::assert_display_matches_label`]
1643 /// primitive so the sweep body lives at ONE substrate site rather
1644 /// than restated per-implementor. Also exercised through the
1645 /// substrate-wide `every_production_display_impl_binds_through_the_testkit_primitive`
1646 /// sweep so a per-crate test-site drop cannot silently disable the
1647 /// check.
1648 #[test]
1649 fn report_payload_shape_display_matches_as_str() {
1650 crate::tagged_union::assert_display_matches_label::<ReportPayloadShape>();
1651 }
1652
1653 /// EMBED-FIELD UNIQUENESS: no two shapes write into the same
1654 /// `payload.<field>` key. The worker's embed site is
1655 /// `payload.insert(shape.payload_field().into(), …)`; if two
1656 /// shapes aliased to the same field name, two different report
1657 /// sources arriving in the same export envelope would silently
1658 /// overwrite each other's bytes. Caught here.
1659 #[test]
1660 fn report_payload_shape_payload_field_unique_per_variant() {
1661 let mut seen = std::collections::HashSet::new();
1662 for shape in ReportPayloadShape::ALL {
1663 assert!(
1664 seen.insert(shape.payload_field()),
1665 "payload_field collision: {shape:?} → {:?}",
1666 shape.payload_field()
1667 );
1668 }
1669 assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
1670 }
1671
1672 /// TRUTH-TABLE: `payload_field` matches the documented
1673 /// `payload.ndjson` / `payload.raw_b64` shinryu schema. A future
1674 /// rename (e.g. `"raw_b64"` → `"raw"`) lands here at one arm
1675 /// rather than drifting between the docstring prose and the
1676 /// worker's embed-site literal. Adding a third shape forces the
1677 /// author to add a row here (driven by `ALL`), so the table
1678 /// width tracks the closed set.
1679 #[test]
1680 fn report_payload_shape_payload_field_truth_table() {
1681 let table: &[(ReportPayloadShape, &str)] = &[
1682 (ReportPayloadShape::NdJsonLines, "ndjson"),
1683 (ReportPayloadShape::OpaqueBytes, "raw_b64"),
1684 ];
1685 assert_eq!(table.len(), ReportPayloadShape::ALL.len());
1686 for (shape, expected) in table {
1687 assert_eq!(
1688 shape.payload_field(),
1689 *expected,
1690 "payload_field({shape:?}) drift"
1691 );
1692 }
1693 }
1694
1695 /// Every variant's `payload_field` is non-empty and contains no
1696 /// JSON-path-separator (`.`) — the worker concatenates
1697 /// `payload.<payload_field>` so an embedded `.` would alias into
1698 /// the parent map and silently flatten the embed. Structural
1699 /// guard for the field-name shape.
1700 #[test]
1701 fn report_payload_shape_payload_field_is_a_single_segment() {
1702 for shape in ReportPayloadShape::ALL {
1703 let field = shape.payload_field();
1704 assert!(
1705 !field.is_empty(),
1706 "payload_field({shape:?}) is empty — embed site has no destination"
1707 );
1708 assert!(
1709 !field.contains('.'),
1710 "payload_field({shape:?}) contains '.' ({field:?}) — would flatten the embed into payload's parent map"
1711 );
1712 }
1713 }
1714
1715 #[test]
1716 fn export_spec_serde_round_trip() {
1717 let spec = ExportSpec {
1718 source: ArtifactSource {
1719 test_report: Some(TestReportSource {
1720 configmap: "demo-test-results".into(),
1721 key: "junit.xml".into(),
1722 format: ReportFormat::Junit,
1723 namespace: None,
1724 }),
1725 ..ArtifactSource::default()
1726 },
1727 channel: VectorChannel {
1728 http_event: Some(HttpEventChannel::signal("test-report")),
1729 ..VectorChannel::default()
1730 },
1731 when: ExportTrigger::Always,
1732 experiment_id_override: Some("demo-run-2026-05-20".into()),
1733 };
1734
1735 let yaml = serde_yaml::to_string(&spec).unwrap();
1736 // camelCase wire format — what FluxCD / kubectl users see.
1737 assert!(yaml.contains("source:"));
1738 assert!(yaml.contains("testReport:"));
1739 assert!(yaml.contains("configmap: demo-test-results"));
1740 assert!(yaml.contains("format: Junit"));
1741 assert!(yaml.contains("channel:"));
1742 assert!(yaml.contains("httpEvent:"));
1743 assert!(yaml.contains("signalType: test-report"));
1744 assert!(yaml.contains("when: Always"));
1745 assert!(yaml.contains("experimentIdOverride: demo-run-2026-05-20"));
1746
1747 let back: ExportSpec = serde_yaml::from_str(&yaml).unwrap();
1748 assert!(back.source.test_report.is_some());
1749 assert!(back.channel.http_event.is_some());
1750 assert_eq!(back.when, ExportTrigger::Always);
1751 }
1752
1753 #[test]
1754 fn run_marker_labels_round_trip() {
1755 let mut labels = BTreeMap::new();
1756 labels.insert("run-id".into(), "demo-run-2026-05-20".into());
1757 labels.insert("phase".into(), "end".into());
1758 let spec = ExportSpec {
1759 source: ArtifactSource {
1760 run_marker: Some(RunMarkerSource { labels }),
1761 ..ArtifactSource::default()
1762 },
1763 channel: VectorChannel {
1764 http_event: Some(HttpEventChannel::signal("ephemeral-marker")),
1765 ..VectorChannel::default()
1766 },
1767 when: ExportTrigger::Always,
1768 experiment_id_override: None,
1769 };
1770 let yaml = serde_yaml::to_string(&spec).unwrap();
1771 assert!(yaml.contains("runMarker:"));
1772 assert!(yaml.contains("run-id: demo-run-2026-05-20"));
1773 let back: ExportSpec = serde_yaml::from_str(&yaml).unwrap();
1774 let rm = back.source.run_marker.unwrap();
1775 assert_eq!(rm.labels["phase"], "end");
1776 }
1777
1778 /// Default endpoints resolve to the canonical in-cluster Service
1779 /// DNS — a single source of truth other tatara crates can
1780 /// re-export instead of duplicating literals.
1781 #[test]
1782 fn default_endpoints_are_stable_constants() {
1783 assert_eq!(
1784 DEFAULT_VECTOR_INGEST,
1785 "http://vector.observability.svc.cluster.local:8080"
1786 );
1787 assert_eq!(
1788 DEFAULT_NATS_URL,
1789 "nats://nats.observability.svc.cluster.local:4222"
1790 );
1791 }
1792
1793 // ── HttpEventChannel::signal substrate primitive pins ────────────
1794 //
1795 // Sweeps the wire-shape corners the ten pre-lift `HttpEventChannel {
1796 // endpoint: None, signal_type: <s>.into() }` hand-authored fixture
1797 // literals covered — the default-endpoint sentinel projects to
1798 // `None`, the `signal_type` slot rides through verbatim, and the
1799 // downstream [`HttpEventChannel::resolved_endpoint`] read still
1800 // resolves to [`DEFAULT_VECTOR_INGEST`]. A regression that seeded
1801 // a hardcoded endpoint on the composer, dropped the `Into<String>`
1802 // bound so a `&str` caller has to `.to_string()` per-site, or
1803 // reordered the two struct slots surfaces here rather than as
1804 // silent skew at any of the ten downstream fixtures (five in
1805 // `tatara-process::export`, two in `tatara-process::lifetime`, two
1806 // in `tatara-process::tagged_union`, one in
1807 // `tatara-reconciler::render`, one in `tatara-export-worker::lib`).
1808
1809 /// PRIMARY SHAPE: byte-identical parity with the pre-lift 4-token
1810 /// hand-authored literal every fixture spelled. Sweeps every label
1811 /// the ten collapsed sites carry so a regression that dropped or
1812 /// mutated any observable slot's value surfaces HERE rather than
1813 /// downstream. The `endpoint: None` sentinel is the load-bearing
1814 /// slot ([`HttpEventChannel::resolved_endpoint`] gates on `.is_none()`
1815 /// to reach [`DEFAULT_VECTOR_INGEST`]); the pin binds it before the
1816 /// primitive can drift.
1817 #[test]
1818 fn signal_composes_byte_identical_to_pre_lift_literal_across_every_label() {
1819 for label in [
1820 "receipt",
1821 "test-report",
1822 "ephemeral-marker",
1823 "x",
1824 "s",
1825 "demo-run-2026-05-20",
1826 ] {
1827 let via_primitive = HttpEventChannel::signal(label);
1828 let hand_authored = HttpEventChannel {
1829 endpoint: None,
1830 signal_type: label.to_string(),
1831 };
1832 assert_eq!(
1833 via_primitive.endpoint, hand_authored.endpoint,
1834 "signal must project the endpoint slot byte-identically \
1835 to the pre-lift literal on label={label:?}",
1836 );
1837 assert_eq!(
1838 via_primitive.signal_type, hand_authored.signal_type,
1839 "signal must project the signal_type slot byte-identically \
1840 to the pre-lift literal on label={label:?}",
1841 );
1842 assert!(
1843 via_primitive.endpoint.is_none(),
1844 "signal must stamp endpoint: None so resolved_endpoint \
1845 reaches DEFAULT_VECTOR_INGEST on label={label:?}",
1846 );
1847 assert_eq!(
1848 via_primitive.resolved_endpoint(),
1849 DEFAULT_VECTOR_INGEST,
1850 "signal must compose with resolved_endpoint's \
1851 default-fallback gate on label={label:?}",
1852 );
1853 }
1854 }
1855
1856 /// COERCION AXIS PIN: the `impl Into<String>` bound accepts every
1857 /// pre-lift caller shape without a per-site coercion. Pre-lift the
1858 /// ten sites carried three distinct source shapes for the
1859 /// `signal_type` slot: `&'static str` literals with `.into()`
1860 /// (`"receipt".into()`), the export-worker fixture's owned
1861 /// `String` via `.to_string()` (`signal_type.to_string()`), and
1862 /// the property-probe fixtures' short single-char labels. Post-lift
1863 /// EVERY shape reaches the composer through the same `Into<String>`
1864 /// gate; the pin binds that so a future narrowing to `&str` (which
1865 /// would break the export-worker's `signal_type: &str` parameter
1866 /// shape) surfaces here.
1867 #[test]
1868 fn signal_accepts_every_pre_lift_caller_source_shape() {
1869 // Shape 1: `&'static str` literal — every test-fixture site.
1870 let a = HttpEventChannel::signal("receipt");
1871 assert_eq!(a.signal_type, "receipt");
1872 // Shape 2: owned `String` — the export-worker `http_spec`
1873 // fixture pre-lift spelled `signal_type: signal_type.to_string()`
1874 // to project its `&str` parameter into the slot.
1875 let owned: String = "test-report".to_string();
1876 let b = HttpEventChannel::signal(owned);
1877 assert_eq!(b.signal_type, "test-report");
1878 // Shape 3: `&String` — verifies the `Into<String>` bound
1879 // accepts a borrowed owned string without an explicit clone
1880 // (matches the reference shape a caller might reach for after
1881 // an intermediate `let label = String::from("...");` binding).
1882 let borrowed = String::from("ephemeral-marker");
1883 let c = HttpEventChannel::signal(&borrowed[..]);
1884 assert_eq!(c.signal_type, "ephemeral-marker");
1885 }
1886
1887 /// COMPOSITION PIN: `HttpEventChannel::signal` composes byte-
1888 /// identically with [`ChannelKind::select`] on the resolver axis —
1889 /// wrapping the primitive's output in the `VectorChannel` tagged-
1890 /// union slot yields the same `ChannelVariant::HttpEvent(...)`
1891 /// projection as the pre-lift literal did. Guards the primary
1892 /// downstream consumer (the tagged-union `.variant()` resolver
1893 /// every fixture round-trips through) against a regression that
1894 /// projected the primitive onto a non-http-event slot or dropped
1895 /// its `endpoint`/`signal_type` slots between composition sites.
1896 #[test]
1897 fn signal_composes_with_channel_variant_resolver() {
1898 let c = VectorChannel {
1899 http_event: Some(HttpEventChannel::signal("receipt")),
1900 ..VectorChannel::default()
1901 };
1902 match c.variant().unwrap() {
1903 ChannelVariant::HttpEvent(h) => {
1904 assert_eq!(h.signal_type, "receipt");
1905 assert_eq!(h.resolved_endpoint(), DEFAULT_VECTOR_INGEST);
1906 assert!(h.endpoint.is_none());
1907 }
1908 other => panic!("expected HttpEvent, got {other:?}"),
1909 }
1910 // Kind projection through the closed-set discriminator stays
1911 // coherent with `ChannelKind::HttpEvent` — a regression that
1912 // wired the primitive to a non-http-event slot would surface
1913 // here as a wrong-kind panic before any downstream test firing.
1914 let via_kind = ChannelKind::HttpEvent.select(&c).unwrap();
1915 assert_eq!(via_kind.kind(), ChannelKind::HttpEvent);
1916 }
1917
1918 // ── NatsSubjectChannel::publish substrate primitive pins ──────────
1919 //
1920 // Sibling to the HttpEventChannel::signal block above. Sweeps the
1921 // wire-shape corners the six pre-lift `NatsSubjectChannel {
1922 // subject, stream, url: None }` hand-authored fixture literals
1923 // covered — the default-URL sentinel projects to `None`, the
1924 // `subject` and `stream` slots ride through verbatim, and the
1925 // downstream [`NatsSubjectChannel::resolved_url`] read still
1926 // resolves to [`DEFAULT_NATS_URL`]. A regression that seeded a
1927 // hardcoded URL on the composer, dropped the `Into<String>` bound
1928 // on either positional arg so a `&str` caller has to `.to_string()`
1929 // per-site, or reordered the three struct slots surfaces here
1930 // rather than as silent skew at any of the six downstream fixtures
1931 // (two in `tatara-process::export`, one in
1932 // `tatara-process::tagged_union`, one in
1933 // `tatara-reconciler::render`, two in `tatara-export-worker::lib`).
1934
1935 /// PRIMARY SHAPE: byte-identical parity with the pre-lift 3-slot
1936 /// hand-authored literal every fixture spelled. Sweeps every
1937 /// (subject, stream) pair the six collapsed sites carry so a
1938 /// regression that dropped or mutated any observable slot's value
1939 /// surfaces HERE rather than downstream. The `url: None` sentinel
1940 /// is the load-bearing slot ([`NatsSubjectChannel::resolved_url`]
1941 /// gates on `.is_none()` to reach [`DEFAULT_NATS_URL`]); the pin
1942 /// binds it before the primitive can drift.
1943 #[test]
1944 fn publish_composes_byte_identical_to_pre_lift_literal_across_every_subject_stream_pair() {
1945 for (subject, stream) in [
1946 (
1947 "pleme.pleme-dev.ephemeral.{{run_id}}.receipt",
1948 "EPHEMERAL_RECEIPTS",
1949 ),
1950 ("pleme.fixed.subject", "S"),
1951 ("s", "S"),
1952 ("pleme.demo.subject.2026-05-20", "DEMO_STREAM"),
1953 ] {
1954 let via_primitive = NatsSubjectChannel::publish(subject, stream);
1955 let hand_authored = NatsSubjectChannel {
1956 subject: subject.to_string(),
1957 stream: stream.to_string(),
1958 url: None,
1959 };
1960 assert_eq!(
1961 via_primitive.subject, hand_authored.subject,
1962 "publish must project the subject slot byte-identically \
1963 to the pre-lift literal on (subject={subject:?}, stream={stream:?})",
1964 );
1965 assert_eq!(
1966 via_primitive.stream, hand_authored.stream,
1967 "publish must project the stream slot byte-identically \
1968 to the pre-lift literal on (subject={subject:?}, stream={stream:?})",
1969 );
1970 assert_eq!(
1971 via_primitive.url, hand_authored.url,
1972 "publish must project the url slot byte-identically \
1973 to the pre-lift literal on (subject={subject:?}, stream={stream:?})",
1974 );
1975 assert!(
1976 via_primitive.url.is_none(),
1977 "publish must stamp url: None so resolved_url reaches \
1978 DEFAULT_NATS_URL on (subject={subject:?}, stream={stream:?})",
1979 );
1980 assert_eq!(
1981 via_primitive.resolved_url(),
1982 DEFAULT_NATS_URL,
1983 "publish must compose with resolved_url's default-fallback \
1984 gate on (subject={subject:?}, stream={stream:?})",
1985 );
1986 }
1987 }
1988
1989 /// COERCION AXIS PIN: the `impl Into<String>` bound on both
1990 /// positional args accepts every pre-lift caller shape without a
1991 /// per-site coercion. Pre-lift the six sites carried three
1992 /// distinct source shapes: `&'static str` literals with `.into()`
1993 /// (`"S".into()`), owned `String` values, and short single-char
1994 /// property-probe labels. Post-lift EVERY shape reaches the
1995 /// composer through the same `Into<String>` gate; the pin binds
1996 /// that so a future narrowing to `&str` (which would break every
1997 /// worker-crate fixture that pre-lift wrote `subject: <s>.into()`)
1998 /// surfaces here.
1999 #[test]
2000 fn publish_accepts_every_pre_lift_caller_source_shape() {
2001 // Shape 1: `&'static str` literals on both positional args —
2002 // the closed-set property probes' shape.
2003 let a = NatsSubjectChannel::publish("s", "S");
2004 assert_eq!(a.subject, "s");
2005 assert_eq!(a.stream, "S");
2006 // Shape 2: owned `String` values on both positional args — the
2007 // shape a caller reaches for after `let subj = format!(...);`
2008 // or `let stream = String::from(...);` bindings.
2009 let owned_subject: String = "pleme.demo.subject".to_string();
2010 let owned_stream: String = "DEMO_STREAM".to_string();
2011 let b = NatsSubjectChannel::publish(owned_subject, owned_stream);
2012 assert_eq!(b.subject, "pleme.demo.subject");
2013 assert_eq!(b.stream, "DEMO_STREAM");
2014 // Shape 3: mixed — `&str` slice on one arg, owned `String` on
2015 // the other — verifies the two positional bounds are
2016 // independent (a regression that unified them under a single
2017 // generic type parameter `T: Into<String>` would break this).
2018 let subj_slice = String::from("pleme.mixed.subject");
2019 let stream_owned: String = "MIXED_STREAM".to_string();
2020 let c = NatsSubjectChannel::publish(&subj_slice[..], stream_owned);
2021 assert_eq!(c.subject, "pleme.mixed.subject");
2022 assert_eq!(c.stream, "MIXED_STREAM");
2023 }
2024
2025 /// COMPOSITION PIN: `NatsSubjectChannel::publish` composes byte-
2026 /// identically with [`ChannelKind::select`] on the resolver axis —
2027 /// wrapping the primitive's output in the `VectorChannel` tagged-
2028 /// union slot yields the same `ChannelVariant::NatsSubject(...)`
2029 /// projection as the pre-lift literal did. Guards the primary
2030 /// downstream consumer (the tagged-union `.variant()` resolver
2031 /// every fixture round-trips through) against a regression that
2032 /// projected the primitive onto a non-nats-subject slot or dropped
2033 /// its `subject`/`stream`/`url` slots between composition sites.
2034 #[test]
2035 fn publish_composes_with_channel_variant_resolver() {
2036 let c = VectorChannel {
2037 nats_subject: Some(NatsSubjectChannel::publish(
2038 "pleme.pleme-dev.ephemeral.{{run_id}}.receipt",
2039 "EPHEMERAL_RECEIPTS",
2040 )),
2041 ..VectorChannel::default()
2042 };
2043 match c.variant().unwrap() {
2044 ChannelVariant::NatsSubject(n) => {
2045 assert_eq!(n.subject, "pleme.pleme-dev.ephemeral.{{run_id}}.receipt");
2046 assert_eq!(n.stream, "EPHEMERAL_RECEIPTS");
2047 assert_eq!(n.resolved_url(), DEFAULT_NATS_URL);
2048 assert!(n.url.is_none());
2049 }
2050 other => panic!("expected NatsSubject, got {other:?}"),
2051 }
2052 // Kind projection through the closed-set discriminator stays
2053 // coherent with `ChannelKind::NatsSubject` — a regression that
2054 // wired the primitive to a non-nats-subject slot would surface
2055 // here as a wrong-kind panic before any downstream test firing.
2056 let via_kind = ChannelKind::NatsSubject.select(&c).unwrap();
2057 assert_eq!(via_kind.kind(), ChannelKind::NatsSubject);
2058 }
2059
2060 // ── closed-set algebra for ArtifactKind (ALL × as_str × Display ×
2061 // FromStr × select × ArtifactVariant::kind) ─────────────────────
2062
2063 /// Structural well-formedness of [`ArtifactKind`] as a
2064 /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
2065 /// testkit lift that pins all three structural invariants (`ALL`
2066 /// is non-empty, every variant round-trips through
2067 /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
2068 /// outside the closed set) at ONE call site. Replaces the hand-
2069 /// derived `artifact_kind_all_is_unique_and_complete` +
2070 /// `artifact_kind_roundtrip_via_as_str` + the empty-input arm of
2071 /// `unknown_artifact_kind_errors`. `FromStr` delegates to
2072 /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
2073 /// exercises the same code path the export worker hits when
2074 /// parsing a CRD `enum:`-validated value back to the typed kind.
2075 #[test]
2076 fn artifact_kind_is_well_formed_closed_set() {
2077 tatara_closed_set::assert_closed_set_well_formed::<ArtifactKind>();
2078 }
2079
2080 /// CANONICAL-KEY CONTRACT: every `ArtifactKind::as_str()` matches
2081 /// the serde `rename_all = "camelCase"` field name on the
2082 /// corresponding `Option<…>` slot of `ArtifactSource`. A future
2083 /// rename of either the struct field OR the `as_str` arm lands
2084 /// here at one site, instead of drifting between the typed
2085 /// surface, the YAML wire format, and the `ArtifactError::Empty`
2086 /// diagnostic. The mapping is the table the serde derive produces
2087 /// against the struct field declarations above; reading the YAML
2088 /// output pins it without re-deriving by hand.
2089 ///
2090 /// Routes through the substrate primitive
2091 /// [`crate::tagged_union::assert_single_slot_key_matches_label`],
2092 /// which pins the exactly-one-key + name-equality projection
2093 /// byte-identically for every `<T: TaggedUnion + Serialize>`
2094 /// implementor — the wire-alignment testkit shared with the sibling
2095 /// `intent_kind_as_str_matches_intent_field_name` /
2096 /// `encapsulation_target_as_str_matches_field_name` /
2097 /// `channel_kind_as_str_matches_field_name` sites. Pre-lift this
2098 /// site restated a weaker YAML-substring check (`yaml.contains(&format!("{key}:"))`)
2099 /// which would silently pass on drift where a non-tagged-union
2100 /// field was added to `ArtifactSource`; post-lift the primitive's
2101 /// JSON exactly-one form catches that drift too — at ONE substrate
2102 /// site.
2103 #[test]
2104 fn artifact_kind_as_str_matches_field_name() {
2105 crate::tagged_union::assert_single_slot_key_matches_label::<ArtifactSource, _>(
2106 single_slot_source,
2107 );
2108 }
2109
2110 /// CANONICAL-NAMES PIN: byte-exact camelCase wire-format pin —
2111 /// renaming any of these strings IS a wire-format break that fails
2112 /// this test FIRST so the rename stays a deliberate decision, not
2113 /// a typo. Locks the (variant → operator-facing key) table.
2114 #[test]
2115 fn artifact_kind_canonical_names_pinned() {
2116 assert_eq!(ArtifactKind::Receipts.as_str(), "receipts");
2117 assert_eq!(ArtifactKind::TestReport.as_str(), "testReport");
2118 assert_eq!(ArtifactKind::ProcessSnapshot.as_str(), "processSnapshot");
2119 assert_eq!(ArtifactKind::RunMarker.as_str(), "runMarker");
2120 }
2121
2122 /// The Display impl IS `as_str` — pinning this lets future callers
2123 /// reach for either projection without drift. If a reviewer
2124 /// accidentally re-introduces an inline match in Display, this
2125 /// test would fail the moment a variant rename touches one site
2126 /// but not the other.
2127 #[test]
2128 fn artifact_kind_display_matches_as_str() {
2129 crate::tagged_union::assert_display_matches_label::<ArtifactKind>();
2130 }
2131
2132 /// `FromStr` rejects strings that aren't in the canonical
2133 /// projection — PascalCased / typo / cross-axis-leaked inputs
2134 /// from sibling closed-set enums on the same `ExportSpec` axis
2135 /// (`Junit`, `OnAttested`, …) — and the error echoes the input
2136 /// verbatim so the operator-facing diagnostic carries the
2137 /// offending value, not a normalized form. `ArtifactKind` is its
2138 /// own axis, NOT a transparent reflection of any sibling. The
2139 /// empty-input arm is pinned by
2140 /// [`artifact_kind_is_well_formed_closed_set`] via the
2141 /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
2142 /// verbatim-echo contract on the [`UnknownArtifactKind`] newtype,
2143 /// which the trait's `make_unknown` can't see.
2144 #[test]
2145 fn unknown_artifact_kind_errors() {
2146 use std::str::FromStr;
2147 for bad in [
2148 "Receipts",
2149 "test_report",
2150 "RECEIPTS",
2151 "snapshot",
2152 "marker",
2153 "Junit",
2154 "OnAttested",
2155 "NdJsonLines",
2156 ] {
2157 let err = ArtifactKind::from_str(bad).unwrap_err();
2158 assert_eq!(err.0, bad, "error payload should echo input verbatim");
2159 }
2160 }
2161
2162 /// ROUND-TRIP CONTRACT: every kind reaches its borrowed-variant
2163 /// view via `select`, and that variant projects back to the same
2164 /// kind via `ArtifactVariant::kind`. A regression that misroutes a
2165 /// select arm (e.g. `Self::Receipts => source.test_report.as_ref()
2166 /// ...`) fails loudly here.
2167 ///
2168 /// Routes through the substrate primitive
2169 /// [`crate::tagged_union::assert_variant_round_trip`] shared with
2170 /// the sibling `intent_kind_round_trips_through_variant_kind` /
2171 /// `channel_kind_round_trips_through_variant_kind` /
2172 /// `encapsulation_target_round_trips_through_variant_target`
2173 /// sites — the projection lives at ONE substrate primitive and
2174 /// every site binds through a single call.
2175 #[test]
2176 fn artifact_kind_round_trips_through_variant_kind() {
2177 crate::tagged_union::assert_variant_round_trip::<ArtifactSource, _>(single_slot_source);
2178 }
2179
2180 /// SELECT-EMPTY CONTRACT: an unpopulated slot returns `None` from
2181 /// `select`, for every kind. Pairs with the resolver's `Empty`
2182 /// path so a future kind's slot defaulting wrong (e.g. accidentally
2183 /// `Some(Default::default())` instead of `None`) is caught here.
2184 #[test]
2185 fn artifact_kind_select_returns_none_for_unset_slot() {
2186 let empty = ArtifactSource::default();
2187 for kind in ArtifactKind::ALL {
2188 assert!(
2189 kind.select(&empty).is_none(),
2190 "{kind:?} reported populated on a default ArtifactSource"
2191 );
2192 }
2193 }
2194
2195 /// EMPTY-DIAGNOSTIC CONTRACT: the closed-set kind list embedded
2196 /// in `ArtifactError::Empty` echoes the canonical join of every
2197 /// `ArtifactKind::as_str()` projection. A variant added without
2198 /// updating `ARTIFACT_KIND_LIST` (or a renamed variant) shows up
2199 /// here as a mismatch. Routes through the substrate primitive
2200 /// [`crate::tagged_union::assert_kind_list_matches_closed_set`]
2201 /// shared with the sibling
2202 /// `intent_error_empty_lists_every_kind_in_canonical_order`
2203 /// / `channel_error_empty_lists_every_kind_in_canonical_order`
2204 /// / `encapsulation_kind_error_empty_lists_every_target_in_canonical_order`
2205 /// sites — the projection lives at ONE substrate primitive and
2206 /// every site binds through a single call.
2207 #[test]
2208 fn artifact_error_empty_lists_every_kind_in_canonical_order() {
2209 crate::tagged_union::assert_kind_list_matches_closed_set::<ArtifactSource>();
2210 }
2211
2212 /// AMBIGUOUS-PATH CONTRACT: when two slots are populated the
2213 /// resolver yields `Ambiguous`, exhaustively across every pair in
2214 /// `ALL × ALL` (excluding the diagonal). A future asymmetry where
2215 /// one slot would silently shadow another (e.g. an `if-let` chain
2216 /// re-introducing first-wins ordering) is caught here. Routes
2217 /// through the substrate primitive
2218 /// [`crate::tagged_union::assert_two_slots_ambiguous`] shared with
2219 /// the sibling
2220 /// `encapsulation_kind_two_slots_is_ambiguous_across_every_pair`
2221 /// / `vector_channel_two_slots_is_ambiguous_across_every_pair`
2222 /// / `intent_two_slots_is_ambiguous_across_every_pair` sites — the
2223 /// nested-`for a in K::ALL { for b in K::ALL { … } }` sweep lives
2224 /// at ONE substrate site.
2225 #[test]
2226 fn artifact_source_two_slots_is_ambiguous_across_every_pair() {
2227 crate::tagged_union::assert_two_slots_ambiguous::<ArtifactSource, _>(two_slot_source);
2228 }
2229
2230 // ── closed-set algebra for ChannelKind (ALL × as_str × Display ×
2231 // FromStr × select × ChannelVariant::kind) ─────────────────────
2232
2233 /// Structural well-formedness of [`ChannelKind`] as a
2234 /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
2235 /// testkit lift that pins all three structural invariants (`ALL`
2236 /// is non-empty, every variant round-trips through
2237 /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
2238 /// outside the closed set) at ONE call site. Replaces the hand-
2239 /// derived `channel_kind_all_is_unique_and_complete` +
2240 /// `channel_kind_roundtrip_via_as_str` + the empty-input arm of
2241 /// `unknown_channel_kind_errors`. `FromStr` delegates to
2242 /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
2243 /// exercises the same code path the export worker hits when
2244 /// parsing a CRD `enum:`-validated value back to the typed kind.
2245 #[test]
2246 fn channel_kind_is_well_formed_closed_set() {
2247 tatara_closed_set::assert_closed_set_well_formed::<ChannelKind>();
2248 }
2249
2250 /// CANONICAL-KEY CONTRACT: every `ChannelKind::as_str()` matches
2251 /// the serde `rename_all = "camelCase"` field name on the
2252 /// corresponding `Option<…>` slot of `VectorChannel`. A future
2253 /// rename of either the struct field OR the `as_str` arm lands
2254 /// here at one site, instead of drifting between the typed
2255 /// surface, the YAML wire format, and the `ChannelError::Empty`
2256 /// diagnostic.
2257 ///
2258 /// Routes through the substrate primitive
2259 /// [`crate::tagged_union::assert_single_slot_key_matches_label`],
2260 /// which pins the exactly-one-key + name-equality projection
2261 /// byte-identically for every `<T: TaggedUnion + Serialize>`
2262 /// implementor — the wire-alignment testkit shared with the sibling
2263 /// `intent_kind_as_str_matches_intent_field_name` /
2264 /// `encapsulation_target_as_str_matches_field_name` /
2265 /// `artifact_kind_as_str_matches_field_name` sites. Pre-lift this
2266 /// site restated a weaker YAML-substring check (`yaml.contains(&format!("{key}:"))`)
2267 /// which would silently pass on drift where a non-tagged-union
2268 /// field was added to `VectorChannel`; post-lift the primitive's
2269 /// JSON exactly-one form catches that drift too — at ONE substrate
2270 /// site.
2271 #[test]
2272 fn channel_kind_as_str_matches_field_name() {
2273 crate::tagged_union::assert_single_slot_key_matches_label::<VectorChannel, _>(
2274 single_slot_channel,
2275 );
2276 }
2277
2278 /// CANONICAL-NAMES PIN: byte-exact camelCase wire-format pin —
2279 /// renaming any of these strings IS a wire-format break that fails
2280 /// this test FIRST so the rename stays a deliberate decision, not
2281 /// a typo. Locks the (variant → operator-facing key) table.
2282 #[test]
2283 fn channel_kind_canonical_names_pinned() {
2284 assert_eq!(ChannelKind::HttpEvent.as_str(), "httpEvent");
2285 assert_eq!(ChannelKind::NatsSubject.as_str(), "natsSubject");
2286 assert_eq!(ChannelKind::Stdout.as_str(), "stdout");
2287 }
2288
2289 /// The Display impl IS `as_str` — pinning this lets future callers
2290 /// reach for either projection without drift.
2291 #[test]
2292 fn channel_kind_display_matches_as_str() {
2293 crate::tagged_union::assert_display_matches_label::<ChannelKind>();
2294 }
2295
2296 /// `FromStr` rejects strings that aren't in the canonical
2297 /// projection — PascalCased / typo / cross-axis-leaked inputs
2298 /// from sibling closed-set enums on the same `ExportSpec` axis
2299 /// (`Receipts`, `OnAttested`, `Junit`, …) — and the error echoes
2300 /// the input verbatim so the operator-facing diagnostic carries
2301 /// the offending value, not a normalized form. `ChannelKind` is
2302 /// its own axis, NOT a transparent reflection of any sibling. The
2303 /// empty-input arm is pinned by
2304 /// [`channel_kind_is_well_formed_closed_set`] via the
2305 /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
2306 /// verbatim-echo contract on the [`UnknownChannelKind`] newtype,
2307 /// which the trait's `make_unknown` can't see.
2308 #[test]
2309 fn unknown_channel_kind_errors() {
2310 use std::str::FromStr;
2311 for bad in [
2312 "HttpEvent",
2313 "http_event",
2314 "HTTPEVENT",
2315 "nats",
2316 "STDOUT",
2317 "Receipts",
2318 "OnAttested",
2319 "Junit",
2320 "NdJsonLines",
2321 ] {
2322 let err = ChannelKind::from_str(bad).unwrap_err();
2323 assert_eq!(err.0, bad, "error payload should echo input verbatim");
2324 }
2325 }
2326
2327 // `unknown_channel_kind_message_matches_substrate_convention`
2328 // removed — clause (5) of
2329 // `tatara_closed_set::assert_closed_set_well_formed::<ChannelKind>()`
2330 // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
2331 // shape generically (called from `channel_kind_is_well_formed_closed_set`
2332 // above); the `SET_LABEL` projection is pinned by
2333 // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
2334
2335 /// ROUND-TRIP CONTRACT: every kind reaches its borrowed-variant
2336 /// view via `select`, and that variant projects back to the same
2337 /// kind via `ChannelVariant::kind`. A regression that misroutes a
2338 /// select arm (e.g. `Self::HttpEvent => channel.nats_subject ...`)
2339 /// fails loudly here.
2340 ///
2341 /// Routes through the substrate primitive
2342 /// [`crate::tagged_union::assert_variant_round_trip`] shared with
2343 /// the sibling `intent_kind_round_trips_through_variant_kind` /
2344 /// `artifact_kind_round_trips_through_variant_kind` /
2345 /// `encapsulation_target_round_trips_through_variant_target`
2346 /// sites — the projection lives at ONE substrate primitive and
2347 /// every site binds through a single call.
2348 #[test]
2349 fn channel_kind_round_trips_through_variant_kind() {
2350 crate::tagged_union::assert_variant_round_trip::<VectorChannel, _>(single_slot_channel);
2351 }
2352
2353 /// SELECT-EMPTY CONTRACT: an unpopulated slot returns `None` from
2354 /// `select`, for every kind. Pairs with the resolver's `Empty`
2355 /// path so a future kind's slot defaulting wrong (e.g. accidentally
2356 /// `Some(Default::default())` instead of `None`) is caught here.
2357 #[test]
2358 fn channel_kind_select_returns_none_for_unset_slot() {
2359 let empty = VectorChannel::default();
2360 for kind in ChannelKind::ALL {
2361 assert!(
2362 kind.select(&empty).is_none(),
2363 "{kind:?} reported populated on a default VectorChannel"
2364 );
2365 }
2366 }
2367
2368 /// EMPTY-DIAGNOSTIC CONTRACT: the closed-set kind list embedded
2369 /// in `ChannelError::Empty` echoes the canonical join of every
2370 /// `ChannelKind::as_str()` projection. A variant added without
2371 /// updating `CHANNEL_KIND_LIST` (or a renamed variant) shows up
2372 /// here as a mismatch. Routes through the substrate primitive
2373 /// [`crate::tagged_union::assert_kind_list_matches_closed_set`]
2374 /// shared with the sibling `intent_error_empty_lists_every_kind_in_canonical_order`
2375 /// / `artifact_error_empty_lists_every_kind_in_canonical_order`
2376 /// / `encapsulation_kind_error_empty_lists_every_target_in_canonical_order`
2377 /// sites — the projection lives at ONE substrate primitive and
2378 /// every site binds through a single call.
2379 #[test]
2380 fn channel_error_empty_lists_every_kind_in_canonical_order() {
2381 crate::tagged_union::assert_kind_list_matches_closed_set::<VectorChannel>();
2382 }
2383
2384 /// AMBIGUOUS-PATH CONTRACT: when two slots are populated the
2385 /// resolver yields `Ambiguous`, exhaustively across every pair in
2386 /// `ALL × ALL` (excluding the diagonal). A future asymmetry where
2387 /// one slot would silently shadow another (e.g. an `if-let` chain
2388 /// re-introducing first-wins ordering) is caught here. Routes
2389 /// through the substrate primitive
2390 /// [`crate::tagged_union::assert_two_slots_ambiguous`] shared with
2391 /// the sibling
2392 /// `encapsulation_kind_two_slots_is_ambiguous_across_every_pair`
2393 /// / `artifact_source_two_slots_is_ambiguous_across_every_pair`
2394 /// / `intent_two_slots_is_ambiguous_across_every_pair` sites — the
2395 /// nested-`for a in K::ALL { for b in K::ALL { … } }` sweep lives
2396 /// at ONE substrate site.
2397 #[test]
2398 fn vector_channel_two_slots_is_ambiguous_across_every_pair() {
2399 crate::tagged_union::assert_two_slots_ambiguous::<VectorChannel, _>(two_slot_channel);
2400 }
2401
2402 /// Construct a `VectorChannel` with exactly the given kind's slot
2403 /// populated by a minimal valid inner channel. Shared across the
2404 /// closed-set property tests so they each cover every variant
2405 /// without restating the construction table. Mirrors
2406 /// `single_slot_source` in shape.
2407 fn single_slot_channel(kind: ChannelKind) -> VectorChannel {
2408 match kind {
2409 ChannelKind::HttpEvent => VectorChannel {
2410 http_event: Some(HttpEventChannel::signal("x")),
2411 ..VectorChannel::default()
2412 },
2413 ChannelKind::NatsSubject => VectorChannel {
2414 nats_subject: Some(NatsSubjectChannel::publish("s", "S")),
2415 ..VectorChannel::default()
2416 },
2417 ChannelKind::Stdout => VectorChannel {
2418 stdout: Some(StdoutChannel::default()),
2419 ..VectorChannel::default()
2420 },
2421 }
2422 }
2423
2424 /// Construct a `VectorChannel` with two slots populated — drives
2425 /// the pairwise `Ambiguous` sweep. Composes the single-slot
2426 /// constructor on top of itself to keep one source of truth for
2427 /// per-variant inner payloads.
2428 fn two_slot_channel(a: ChannelKind, b: ChannelKind) -> VectorChannel {
2429 let ca = single_slot_channel(a);
2430 let cb = single_slot_channel(b);
2431 VectorChannel {
2432 http_event: ca.http_event.or(cb.http_event),
2433 nats_subject: ca.nats_subject.or(cb.nats_subject),
2434 stdout: ca.stdout.or(cb.stdout),
2435 }
2436 }
2437
2438 /// Construct an `ArtifactSource` with exactly the given kind's
2439 /// slot populated by a minimal valid inner source. Shared across
2440 /// the closed-set property tests so they each cover every variant
2441 /// without restating the construction table. Mirrors
2442 /// `single_slot_intent` in shape.
2443 fn single_slot_source(kind: ArtifactKind) -> ArtifactSource {
2444 match kind {
2445 ArtifactKind::Receipts => ArtifactSource {
2446 receipts: Some(ReceiptsSource::default()),
2447 ..ArtifactSource::default()
2448 },
2449 ArtifactKind::TestReport => ArtifactSource {
2450 test_report: Some(TestReportSource {
2451 configmap: "cm".into(),
2452 key: "k".into(),
2453 format: ReportFormat::Junit,
2454 namespace: None,
2455 }),
2456 ..ArtifactSource::default()
2457 },
2458 ArtifactKind::ProcessSnapshot => ArtifactSource {
2459 process_snapshot: Some(ProcessSnapshotSource::default()),
2460 ..ArtifactSource::default()
2461 },
2462 ArtifactKind::RunMarker => ArtifactSource {
2463 run_marker: Some(RunMarkerSource::default()),
2464 ..ArtifactSource::default()
2465 },
2466 }
2467 }
2468
2469 /// Construct an `ArtifactSource` with two slots populated — drives
2470 /// the pairwise `Ambiguous` sweep. Composes the single-slot
2471 /// constructor on top of itself to keep one source of truth for
2472 /// per-variant inner payloads.
2473 fn two_slot_source(a: ArtifactKind, b: ArtifactKind) -> ArtifactSource {
2474 // Merge by populating each kind's slot from its single-slot view.
2475 let sa = single_slot_source(a);
2476 let sb = single_slot_source(b);
2477 ArtifactSource {
2478 receipts: sa.receipts.or(sb.receipts),
2479 test_report: sa.test_report.or(sb.test_report),
2480 process_snapshot: sa.process_snapshot.or(sb.process_snapshot),
2481 run_marker: sa.run_marker.or(sb.run_marker),
2482 }
2483 }
2484
2485 // ── ExportSpecSliceExt::has_when substrate pins ───────────────────
2486 //
2487 // Fail-before-pass-after granularity: `ExportSpecSliceExt` did not
2488 // exist before this commit — the `(&[ExportSpec], ExportTrigger)
2489 // -> bool` walk shape was not spelled anywhere in the workspace on
2490 // the raw `when` field. The lift opens the FOURTH instance in the
2491 // slice-level closed-set-driven presence-probe algebra (peer of
2492 // `ConditionSliceExt::has_kind` on `&[Condition]`,
2493 // `DependsOnSliceExt::has_must_reach` on `&[DependsOn]`, and
2494 // `ComplianceBindingSliceExt::has_verification_phase` on
2495 // `&[ComplianceBinding]`), enabling the seventh
2496 // `export-when-<kind>` require-tag prefix family in
2497 // `tatara-reconciler::bin::tatara-check` to compose against ONE
2498 // substrate site rather than restating the `.iter().any(|e| e.when
2499 // == K)` closure body inline at the classifier.
2500
2501 /// Fixture: a minimal `ExportSpec` with a chosen `when` trigger
2502 /// and a single-slot receipts source + stdout channel. The trigger
2503 /// is the only axis this test module discriminates on; the source
2504 /// + channel are fixed at valid single-slot pairs so the primitive
2505 /// under test reads the `when` field in isolation.
2506 fn export_at(when: ExportTrigger) -> ExportSpec {
2507 ExportSpec {
2508 source: ArtifactSource {
2509 receipts: Some(ReceiptsSource::default()),
2510 ..ArtifactSource::default()
2511 },
2512 channel: VectorChannel {
2513 stdout: Some(StdoutChannel::default()),
2514 ..VectorChannel::default()
2515 },
2516 when,
2517 experiment_id_override: None,
2518 }
2519 }
2520
2521 /// EMPTY-SLICE pin — an empty `&[ExportSpec]` returns `false` for
2522 /// EVERY [`ExportTrigger`]. Sweep [`ExportTrigger::ALL`] so a new
2523 /// variant added without a matching arm in the primitive surfaces
2524 /// at rustc's exhaustiveness gate on the `ALL` literal (arity
2525 /// forced by `[Self; 3]`) rather than as a silent false-positive
2526 /// at every downstream callsite composing this primitive.
2527 #[test]
2528 fn export_spec_slice_has_when_returns_false_on_empty_slice_for_every_kind() {
2529 let empty: &[ExportSpec] = &[];
2530 for kind in ExportTrigger::ALL {
2531 assert!(
2532 !empty.has_when(kind),
2533 "empty slice must return false for {kind:?}",
2534 );
2535 }
2536 }
2537
2538 /// PER-VARIANT pin — a single-element slice returns `true` for
2539 /// exactly the trigger it carries, `false` for every other
2540 /// variant. Sweep the [`ExportTrigger::ALL`] × ALL cross so a
2541 /// regression that (a) hard-coded the arm to a single kind
2542 /// (silently returning true for every populated slice regardless
2543 /// of query kind), or (b) matched on a different field (a stray
2544 /// `experiment_id_override.is_some()`, a `source`-side variant
2545 /// discriminator) fails HERE at the substrate primitive rather
2546 /// than at each downstream `export-when-<kind>` callsite.
2547 #[test]
2548 fn export_spec_slice_has_when_reads_when_field_per_variant() {
2549 for populated in ExportTrigger::ALL {
2550 let slice = [export_at(populated)];
2551 for query in ExportTrigger::ALL {
2552 let expected = query == populated;
2553 assert_eq!(
2554 slice.has_when(query),
2555 expected,
2556 "populated={populated:?}: query {query:?} drifted",
2557 );
2558 }
2559 }
2560 }
2561
2562 /// MULTI-ENTRY pin — a slice with multiple entries returns `true`
2563 /// for every trigger that appears at any position (existential
2564 /// quantifier over the slice), `false` for triggers that appear
2565 /// at no position. Locks the `any` semantics so a regression that
2566 /// collapsed to a `first`-only probe (`slice.first().is_some_and(
2567 /// |e| e.when == kind)`) fails here even though the single-element
2568 /// per-variant pin above passes.
2569 #[test]
2570 fn export_spec_slice_has_when_scans_beyond_the_first_position() {
2571 let slice = [
2572 export_at(ExportTrigger::OnAttested),
2573 export_at(ExportTrigger::Always),
2574 ];
2575 for present in [ExportTrigger::OnAttested, ExportTrigger::Always] {
2576 assert!(
2577 slice.has_when(present),
2578 "trigger at any position must resolve true: {present:?}",
2579 );
2580 }
2581 assert!(
2582 !slice.has_when(ExportTrigger::OnFailed),
2583 "trigger absent from the slice must resolve false: OnFailed",
2584 );
2585 }
2586
2587 // ── ExportSpecSliceExt::has_channel_kind substrate pins ───────────
2588 //
2589 // Fail-before-pass-after granularity: `has_channel_kind` did not
2590 // exist before this commit — the `(&[ExportSpec], ChannelKind) ->
2591 // bool` walk shape was not spelled anywhere in the workspace on the
2592 // tagged-union `channel` field. The lift opens the SECOND method
2593 // on the slice-level `ExportSpecSliceExt` (peer of `has_when` on
2594 // the same slice, and fifth instance across the workspace slice-
2595 // level closed-set-driven presence-probe algebra), composing
2596 // `ChannelKind::select` — the ONE substrate owner of the "is the
2597 // slot populated" projection for a tagged-union carrier — with the
2598 // same `.iter().any(|e| …)` walk shape `has_when` publishes.
2599
2600 /// Fixture: a minimal `ExportSpec` with a chosen [`ChannelKind`]
2601 /// populated on its `channel` slot and a fixed single-slot
2602 /// receipts source + default `when` trigger. The channel kind is
2603 /// the only axis this test module discriminates on; the source +
2604 /// trigger are fixed at valid pairs so the primitive under test
2605 /// reads the `channel` slot in isolation.
2606 ///
2607 /// Sweeps [`ChannelKind::ALL`] via `match` on the closed set so a
2608 /// future fourth variant added to `ALL` reaches this fixture at
2609 /// rustc's exhaustiveness gate on the `match` arm — the same
2610 /// exhaustive-match contract [`ChannelKind::select`] publishes.
2611 fn export_with_channel(kind: ChannelKind) -> ExportSpec {
2612 let channel = match kind {
2613 ChannelKind::HttpEvent => VectorChannel {
2614 http_event: Some(HttpEventChannel::signal("test-report")),
2615 ..VectorChannel::default()
2616 },
2617 ChannelKind::NatsSubject => VectorChannel {
2618 nats_subject: Some(NatsSubjectChannel::publish("s", "STREAM")),
2619 ..VectorChannel::default()
2620 },
2621 ChannelKind::Stdout => VectorChannel {
2622 stdout: Some(StdoutChannel::default()),
2623 ..VectorChannel::default()
2624 },
2625 };
2626 ExportSpec {
2627 source: ArtifactSource {
2628 receipts: Some(ReceiptsSource::default()),
2629 ..ArtifactSource::default()
2630 },
2631 channel,
2632 when: ExportTrigger::default(),
2633 experiment_id_override: None,
2634 }
2635 }
2636
2637 /// EMPTY-SLICE pin — an empty `&[ExportSpec]` returns `false` for
2638 /// EVERY [`ChannelKind`]. Sweep [`ChannelKind::ALL`] so a new
2639 /// variant added without a matching arm in `ChannelKind::select`
2640 /// surfaces at rustc's exhaustiveness gate on the `ALL` literal
2641 /// (arity forced by `[Self; 3]`) rather than as a silent false-
2642 /// positive at every downstream callsite composing this primitive.
2643 #[test]
2644 fn export_spec_slice_has_channel_kind_returns_false_on_empty_slice_for_every_kind() {
2645 let empty: &[ExportSpec] = &[];
2646 for kind in ChannelKind::ALL {
2647 assert!(
2648 !empty.has_channel_kind(kind),
2649 "empty slice must return false for {kind:?}",
2650 );
2651 }
2652 }
2653
2654 /// PER-VARIANT pin — a single-element slice returns `true` for
2655 /// exactly the channel kind whose slot is populated, `false` for
2656 /// every other variant. Sweep the [`ChannelKind::ALL`] × ALL cross
2657 /// so a regression that (a) hard-coded the arm to a single kind
2658 /// (silently returning `true` for every populated slice regardless
2659 /// of query kind), or (b) probed on a different field (a stray
2660 /// `experiment_id_override.is_some()`, a `source`-side variant
2661 /// discriminator, `when`) fails HERE at the substrate primitive
2662 /// rather than at each downstream `channel-<kind>` callsite.
2663 #[test]
2664 fn export_spec_slice_has_channel_kind_reads_channel_slot_per_variant() {
2665 for populated in ChannelKind::ALL {
2666 let slice = [export_with_channel(populated)];
2667 for query in ChannelKind::ALL {
2668 let expected = query == populated;
2669 assert_eq!(
2670 slice.has_channel_kind(query),
2671 expected,
2672 "populated={populated:?}: query {query:?} drifted",
2673 );
2674 }
2675 }
2676 }
2677
2678 /// MULTI-ENTRY pin — a slice with multiple entries returns `true`
2679 /// for every channel kind that appears at any position (existential
2680 /// quantifier over the slice), `false` for kinds that appear at no
2681 /// position. Locks the `any` semantics so a regression that
2682 /// collapsed to a `first`-only probe (`slice.first().is_some_and(
2683 /// |e| kind.select(&e.channel).is_some())`) fails here even though
2684 /// the single-element per-variant pin above passes.
2685 #[test]
2686 fn export_spec_slice_has_channel_kind_scans_beyond_the_first_position() {
2687 let slice = [
2688 export_with_channel(ChannelKind::Stdout),
2689 export_with_channel(ChannelKind::NatsSubject),
2690 ];
2691 for present in [ChannelKind::Stdout, ChannelKind::NatsSubject] {
2692 assert!(
2693 slice.has_channel_kind(present),
2694 "channel kind at any position must resolve true: {present:?}",
2695 );
2696 }
2697 assert!(
2698 !slice.has_channel_kind(ChannelKind::HttpEvent),
2699 "channel kind absent from the slice must resolve false: HttpEvent",
2700 );
2701 }
2702
2703 // ── ExportSpecSliceExt::has_report_format substrate pins ──────────
2704 //
2705 // Fail-before-pass-after granularity: `has_report_format` did not
2706 // exist before this commit — the `(&[ExportSpec], ReportFormat) ->
2707 // bool` walk shape was not spelled anywhere in the workspace on the
2708 // nested-Option `source.test_report.format` field. The lift opens
2709 // the THIRD method on the slice-level `ExportSpecSliceExt` (peer of
2710 // `has_when` + `has_channel_kind` on the SAME slice, and sixth
2711 // instance across the workspace slice-level closed-set-driven
2712 // presence-probe algebra), composing an inner `.as_ref().is_some_and`
2713 // gate on the `Option<TestReportSource>` field with a raw
2714 // `ReportFormat` equality on the populated slot — the FIRST probe
2715 // on this trait whose closure reads a NESTED-Option scalar rather
2716 // than a tagged-union `select` or a direct closed-set field.
2717
2718 /// Fixture: a minimal `ExportSpec` whose `source` carries a
2719 /// [`TestReportSource`] tagged with the chosen [`ReportFormat`] and
2720 /// a fixed single-slot stdout channel + default `when` trigger. The
2721 /// report format is the only axis this test module discriminates
2722 /// on; the channel + trigger + configmap/key strings are fixed at
2723 /// valid pairs so the primitive under test reads the
2724 /// `source.test_report.format` slot in isolation.
2725 ///
2726 /// Sweeps [`ReportFormat::ALL`] via the [`ReportFormat`] closed set
2727 /// so a future fifth variant reaches this fixture by hitting the
2728 /// `ALL` array literal's arity gate at every call site.
2729 fn export_with_report_format(kind: ReportFormat) -> ExportSpec {
2730 ExportSpec {
2731 source: ArtifactSource {
2732 test_report: Some(TestReportSource {
2733 configmap: "junit-results".into(),
2734 key: "junit.xml".into(),
2735 format: kind,
2736 namespace: None,
2737 }),
2738 ..ArtifactSource::default()
2739 },
2740 channel: VectorChannel {
2741 stdout: Some(StdoutChannel::default()),
2742 ..VectorChannel::default()
2743 },
2744 when: ExportTrigger::default(),
2745 experiment_id_override: None,
2746 }
2747 }
2748
2749 /// EMPTY-SLICE pin — an empty `&[ExportSpec]` returns `false` for
2750 /// EVERY [`ReportFormat`]. Sweep [`ReportFormat::ALL`] so a new
2751 /// variant added without matching visits in the surrounding
2752 /// substrate reaches rustc's exhaustiveness gate on the `ALL`
2753 /// literal (arity forced by `[Self; 4]`) rather than as a silent
2754 /// false-positive at every downstream callsite composing this
2755 /// primitive.
2756 #[test]
2757 fn export_spec_slice_has_report_format_returns_false_on_empty_slice_for_every_kind() {
2758 let empty: &[ExportSpec] = &[];
2759 for kind in ReportFormat::ALL {
2760 assert!(
2761 !empty.has_report_format(kind),
2762 "empty slice must return false for {kind:?}",
2763 );
2764 }
2765 }
2766
2767 /// PER-VARIANT pin — a single-element slice returns `true` for
2768 /// exactly the [`ReportFormat`] its `TestReportSource` carries,
2769 /// `false` for every other variant. Sweep the [`ReportFormat::ALL`]
2770 /// × ALL cross so a regression that (a) hard-coded the arm to a
2771 /// single kind (silently returning `true` for every populated slice
2772 /// regardless of query kind), (b) probed a different field (a stray
2773 /// `experiment_id_override.is_some()`, a `channel`-side variant
2774 /// discriminator, `when`), or (c) collapsed the outer
2775 /// nested-Option projection (probing `test_report.is_some()` and
2776 /// treating the empty case as `ReportFormat::default() == Raw`)
2777 /// fails HERE at the substrate primitive rather than at each
2778 /// downstream `report-format-<kind>` callsite.
2779 #[test]
2780 fn export_spec_slice_has_report_format_reads_test_report_slot_per_variant() {
2781 for populated in ReportFormat::ALL {
2782 let slice = [export_with_report_format(populated)];
2783 for query in ReportFormat::ALL {
2784 let expected = query == populated;
2785 assert_eq!(
2786 slice.has_report_format(query),
2787 expected,
2788 "populated={populated:?}: query {query:?} drifted",
2789 );
2790 }
2791 }
2792 }
2793
2794 /// MULTI-ENTRY pin — a slice with multiple entries returns `true`
2795 /// for every [`ReportFormat`] that appears at any position
2796 /// (existential quantifier over the slice), `false` for formats
2797 /// that appear at no position. Locks the `any` semantics so a
2798 /// regression that collapsed to a `first`-only probe
2799 /// (`slice.first().is_some_and(|e|
2800 /// e.source.test_report.as_ref().is_some_and(|tr| tr.format ==
2801 /// kind))`) fails here even though the single-element per-variant
2802 /// pin above passes.
2803 #[test]
2804 fn export_spec_slice_has_report_format_scans_beyond_the_first_position() {
2805 let slice = [
2806 export_with_report_format(ReportFormat::Junit),
2807 export_with_report_format(ReportFormat::TapV13),
2808 ];
2809 for present in [ReportFormat::Junit, ReportFormat::TapV13] {
2810 assert!(
2811 slice.has_report_format(present),
2812 "report format at any position must resolve true: {present:?}",
2813 );
2814 }
2815 assert!(
2816 !slice.has_report_format(ReportFormat::NdJson),
2817 "report format absent from the slice must resolve false: NdJson",
2818 );
2819 assert!(
2820 !slice.has_report_format(ReportFormat::Raw),
2821 "report format absent from the slice must resolve false: Raw",
2822 );
2823 }
2824
2825 /// NESTED-OPTION-COLLAPSE pin — an export whose `source` carries
2826 /// NO `test_report` slot (a `receipts`-only source) contributes
2827 /// `false` for EVERY [`ReportFormat`] kind, INCLUDING the default
2828 /// [`ReportFormat::Raw`] that a naive `unwrap_or_default()`
2829 /// projection would spuriously match. Locks the outer
2830 /// nested-`Option` short-circuit contract so a regression that
2831 /// dropped the `.as_ref().is_some_and(…)` gate (e.g. rewriting to
2832 /// `e.source.test_report.map_or(ReportFormat::default(), |tr|
2833 /// tr.format) == kind`, which returns `true` for every non-
2834 /// test-report export when `kind == Raw`) fails HERE at ONE
2835 /// narrow substrate site rather than at every downstream `report-
2836 /// format-Raw` classifier callsite. Sweeps
2837 /// [`ReportFormat::ALL`] so the contract is pinned symmetrically
2838 /// across every format the closed set names.
2839 #[test]
2840 fn export_spec_slice_has_report_format_returns_false_on_non_test_report_source() {
2841 let receipts_only = ExportSpec {
2842 source: ArtifactSource {
2843 receipts: Some(ReceiptsSource::default()),
2844 ..ArtifactSource::default()
2845 },
2846 channel: VectorChannel {
2847 stdout: Some(StdoutChannel::default()),
2848 ..VectorChannel::default()
2849 },
2850 when: ExportTrigger::default(),
2851 experiment_id_override: None,
2852 };
2853 let slice = [receipts_only];
2854 for kind in ReportFormat::ALL {
2855 assert!(
2856 !slice.has_report_format(kind),
2857 "receipts-only export must return false for every report format: {kind:?}",
2858 );
2859 }
2860 }
2861
2862 // ── ExportSpecSliceExt::has_artifact_kind substrate pins ──────────
2863 //
2864 // Fail-before-pass-after granularity: `has_artifact_kind` did not
2865 // exist before this commit — the `(&[ExportSpec], ArtifactKind) ->
2866 // bool` walk shape was not spelled anywhere in the workspace on the
2867 // tagged-union `source` field. The lift opens the FOURTH method on
2868 // the slice-level `ExportSpecSliceExt` (peer of `has_when` +
2869 // `has_channel_kind` + `has_report_format` on the SAME slice, and
2870 // seventh instance across the workspace slice-level closed-set-
2871 // driven presence-probe algebra), composing `ArtifactKind::select`
2872 // — the ONE substrate owner of the "is the slot populated"
2873 // projection for a tagged-union carrier — with the same
2874 // `.iter().any(|e| …)` walk shape the three prior methods publish.
2875 // Sibling to `has_channel_kind` in shape (tagged-union outer
2876 // carrier), distinct from `has_report_format` (nested-Option scalar
2877 // past the outer carrier).
2878
2879 /// Fixture: a minimal `ExportSpec` with a chosen [`ArtifactKind`]
2880 /// populated on its `source` slot and a fixed single-slot stdout
2881 /// channel + default `when` trigger. The artifact kind is the only
2882 /// axis this test module discriminates on; the channel + trigger
2883 /// are fixed at valid pairs so the primitive under test reads the
2884 /// `source` slot in isolation.
2885 ///
2886 /// Sweeps [`ArtifactKind::ALL`] via `match` on the closed set so a
2887 /// future fifth variant added to `ALL` reaches this fixture at
2888 /// rustc's exhaustiveness gate on the `match` arm — the same
2889 /// exhaustive-match contract [`ArtifactKind::select`] publishes.
2890 fn export_with_artifact(kind: ArtifactKind) -> ExportSpec {
2891 let source = match kind {
2892 ArtifactKind::Receipts => ArtifactSource {
2893 receipts: Some(ReceiptsSource::default()),
2894 ..ArtifactSource::default()
2895 },
2896 ArtifactKind::TestReport => ArtifactSource {
2897 test_report: Some(TestReportSource {
2898 configmap: "junit-results".into(),
2899 key: "junit.xml".into(),
2900 format: ReportFormat::Junit,
2901 namespace: None,
2902 }),
2903 ..ArtifactSource::default()
2904 },
2905 ArtifactKind::ProcessSnapshot => ArtifactSource {
2906 process_snapshot: Some(ProcessSnapshotSource::default()),
2907 ..ArtifactSource::default()
2908 },
2909 ArtifactKind::RunMarker => ArtifactSource {
2910 run_marker: Some(RunMarkerSource::default()),
2911 ..ArtifactSource::default()
2912 },
2913 };
2914 ExportSpec {
2915 source,
2916 channel: VectorChannel {
2917 stdout: Some(StdoutChannel::default()),
2918 ..VectorChannel::default()
2919 },
2920 when: ExportTrigger::default(),
2921 experiment_id_override: None,
2922 }
2923 }
2924
2925 /// EMPTY-SLICE pin — an empty `&[ExportSpec]` returns `false` for
2926 /// EVERY [`ArtifactKind`]. Sweep [`ArtifactKind::ALL`] so a new
2927 /// variant added without a matching arm in `ArtifactKind::select`
2928 /// surfaces at rustc's exhaustiveness gate on the `ALL` literal
2929 /// (arity forced by `[Self; 4]`) rather than as a silent false-
2930 /// positive at every downstream callsite composing this primitive.
2931 #[test]
2932 fn export_spec_slice_has_artifact_kind_returns_false_on_empty_slice_for_every_kind() {
2933 let empty: &[ExportSpec] = &[];
2934 for kind in ArtifactKind::ALL {
2935 assert!(
2936 !empty.has_artifact_kind(kind),
2937 "empty slice must return false for {kind:?}",
2938 );
2939 }
2940 }
2941
2942 /// PER-VARIANT pin — a single-element slice returns `true` for
2943 /// exactly the artifact kind whose slot is populated, `false` for
2944 /// every other variant. Sweep the [`ArtifactKind::ALL`] × ALL cross
2945 /// so a regression that (a) hard-coded the arm to a single kind
2946 /// (silently returning `true` for every populated slice regardless
2947 /// of query kind), or (b) probed on a different field (a stray
2948 /// `experiment_id_override.is_some()`, a `channel`-side variant
2949 /// discriminator, `when`) fails HERE at the substrate primitive
2950 /// rather than at each downstream `artifact-<kind>` callsite.
2951 #[test]
2952 fn export_spec_slice_has_artifact_kind_reads_source_slot_per_variant() {
2953 for populated in ArtifactKind::ALL {
2954 let slice = [export_with_artifact(populated)];
2955 for query in ArtifactKind::ALL {
2956 let expected = query == populated;
2957 assert_eq!(
2958 slice.has_artifact_kind(query),
2959 expected,
2960 "populated={populated:?}: query {query:?} drifted",
2961 );
2962 }
2963 }
2964 }
2965
2966 /// MULTI-ENTRY pin — a slice with multiple entries returns `true`
2967 /// for every artifact kind that appears at any position (existential
2968 /// quantifier over the slice), `false` for kinds that appear at no
2969 /// position. Locks the `any` semantics so a regression that
2970 /// collapsed to a `first`-only probe (`slice.first().is_some_and(
2971 /// |e| kind.select(&e.source).is_some())`) fails here even though
2972 /// the single-element per-variant pin above passes.
2973 #[test]
2974 fn export_spec_slice_has_artifact_kind_scans_beyond_the_first_position() {
2975 let slice = [
2976 export_with_artifact(ArtifactKind::RunMarker),
2977 export_with_artifact(ArtifactKind::Receipts),
2978 ];
2979 for present in [ArtifactKind::RunMarker, ArtifactKind::Receipts] {
2980 assert!(
2981 slice.has_artifact_kind(present),
2982 "artifact kind at any position must resolve true: {present:?}",
2983 );
2984 }
2985 for absent in [ArtifactKind::TestReport, ArtifactKind::ProcessSnapshot] {
2986 assert!(
2987 !slice.has_artifact_kind(absent),
2988 "artifact kind absent from the slice must resolve false: {absent:?}",
2989 );
2990 }
2991 }
2992}