1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//! Compliance bindings — CRD-facing with bridges to `tatara_core::compliance_binding`.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tatara_core::domain::compliance_binding as core;
use crate::phase::ProcessPhase;
/// Compliance section of `ProcessSpec`.
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceSpec {
/// Canonical baseline (e.g., `fedramp-moderate`, `cis-k8s-v1.8`, `soc2`, `pci-dss`).
/// Semantically the `meet` of all `bindings`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub baseline: Option<String>,
/// Individual control bindings.
#[serde(default)]
pub bindings: Vec<ComplianceBinding>,
/// Allow the reconciler to invoke remediation hooks on violations.
#[serde(default)]
pub auto_remediate: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceBinding {
/// Framework name: `nist-800-53`, `cis-k8s-v1.8`, `fedramp-moderate`, `soc2`, `pci-dss`.
pub framework: String,
/// Control id within the framework (e.g., `SC-7`, `5.1.1`).
pub control_id: String,
/// When the binding is verified.
#[serde(default)]
pub phase: VerificationPhase,
/// Optional human description.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
/// When a ComplianceBinding is evaluated.
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
JsonSchema,
Default,
tatara_lisp::DeriveClosedSet,
)]
#[serde(rename_all = "PascalCase")]
#[closed_set(via = "as_str", generate_unknown, display)]
pub enum VerificationPhase {
/// Before Execing — fails reconciliation if violated.
PlanTime,
/// During VERIFY — gates Running → Attested.
#[default]
AtBoundary,
/// After Attested — continuous audit, emits events on violation.
PostConvergence,
}
impl VerificationPhase {
/// The closed set of verification phases — single source of truth that
/// drives the `as_str` / Display / `FromStr` triad and the typed
/// `gates_phase` projection over [`ProcessPhase`]. Adding a fourth
/// variant lands at one `ALL` entry + one `as_str` arm + one
/// `gates_phase` arm — exhaustively checked by the compiler (the
/// `[Self; 3]` array literal forces the arity).
///
/// Sibling closed-set lifts on the same `ProcessSpec` axis:
/// [`crate::signal::SighupStrategy::ALL`],
/// [`crate::spec::MustReachPhase::ALL`],
/// [`crate::intent::WorkloadKind::ALL`],
/// [`crate::export::ReportFormat::ALL`],
/// [`crate::encapsulates::EncapsulationMode::ALL`],
/// [`crate::export::ExportTrigger::ALL`],
/// [`crate::lifetime::TeardownPolicy::ALL`],
/// [`crate::boundary::ConditionKind::ALL`],
/// [`crate::lifetime::LifetimeKind::ALL`],
/// [`crate::intent::IntentKind::ALL`],
/// [`crate::phase::ProcessPhase::ALL`],
/// [`crate::signal::ProcessSignal::ALL`].
pub const ALL: [Self; 3] = [Self::PlanTime, Self::AtBoundary, Self::PostConvergence];
/// Canonical PascalCase wire-format projection — matches the serde
/// `rename_all = "PascalCase"` output verbatim AND the CRD `enum:`
/// enumeration the reconciler stamps on the
/// `processes.tatara.pleme.io` schema. Pinned by
/// `verification_phase_as_str_matches_serde` so a variant rename
/// can't drift between the typed surface, the CRD enum, and the
/// YAML wire format at one site.
pub const fn as_str(self) -> &'static str {
match self {
Self::PlanTime => "PlanTime",
Self::AtBoundary => "AtBoundary",
Self::PostConvergence => "PostConvergence",
}
}
/// Typed `const fn` projection onto the [`ProcessPhase`] gate the
/// binding's verification blocks when it fails. Each variant maps
/// to the earliest phase whose entry the binding can prevent:
///
/// - `PlanTime` → `Some(Execing)` — the RENDER phase is what
/// PlanTime gates ("Before Execing — fails reconciliation if
/// violated"); a violated PlanTime control prevents the
/// `Forking → Execing` transition.
/// - `AtBoundary` → `Some(Attested)` — the VERIFY phase ("gates
/// Running → Attested"); a violated AtBoundary control prevents
/// the `Running → Attested` transition.
/// - `PostConvergence` → `None` — the binding is non-blocking
/// ("After Attested — continuous audit, emits events on
/// violation"); it never gates a transition.
///
/// Single source of truth for the future reconciler control-plane
/// compliance evaluator's "which transition would a failing
/// binding block?" decision; pinned by
/// `verification_phase_gates_phase_truth_table`. Closed-set match
/// (not `matches!`) so adding a fourth variant triggers the
/// compiler's exhaustiveness check at this site rather than
/// silently defaulting to either group.
pub const fn gates_phase(self) -> Option<ProcessPhase> {
match self {
Self::PlanTime => Some(ProcessPhase::Execing),
Self::AtBoundary => Some(ProcessPhase::Attested),
Self::PostConvergence => None,
}
}
}
// `impl FromStr for VerificationPhase` +
// `impl tatara_lisp::ClosedSet for VerificationPhase` +
// `impl fmt::Display for VerificationPhase` +
// `pub struct UnknownVerificationPhase(pub String)` are all
// generated by `#[derive(tatara_lisp::DeriveClosedSet)]` +
// `#[closed_set(via = "as_str", generate_unknown, display)]` on the
// enum declaration above. `label` delegates to the inherent
// `VerificationPhase::as_str` (which matches the serde
// `rename_all = "PascalCase"` projection AND the CRD `enum:`
// enumeration verbatim — pinned by
// `verification_phase_as_str_matches_serde`). The auto-derived
// carrier label "verification phase" matches the prior hand-rolled
// `#[error("unknown verification phase: {0}")]` annotation
// byte-for-byte. Symmetric to every other `#[derive(DeriveClosedSet)]`
// implementor across the crate.
impl From<VerificationPhase> for core::VerificationPhase {
fn from(v: VerificationPhase) -> Self {
match v {
VerificationPhase::PlanTime => Self::PlanTime,
VerificationPhase::AtBoundary => Self::AtBoundary,
VerificationPhase::PostConvergence => Self::PostConvergence,
}
}
}
impl From<core::VerificationPhase> for VerificationPhase {
fn from(v: core::VerificationPhase) -> Self {
use core::VerificationPhase as C;
match v {
C::PlanTime => Self::PlanTime,
C::AtBoundary => Self::AtBoundary,
C::PostConvergence => Self::PostConvergence,
}
}
}
impl ComplianceBinding {
pub fn to_core(&self) -> core::ComplianceControl {
core::ComplianceControl {
framework: self.framework.clone(),
control_id: self.control_id.clone(),
description: self.description.clone().unwrap_or_default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_phase_is_at_boundary() {
assert_eq!(VerificationPhase::default(), VerificationPhase::AtBoundary);
}
#[test]
fn binding_roundtrip_to_core() {
let b = ComplianceBinding {
framework: "nist-800-53".into(),
control_id: "SC-7".into(),
phase: VerificationPhase::AtBoundary,
description: Some("boundary protection".into()),
};
let c = b.to_core();
assert_eq!(c.framework, "nist-800-53");
assert_eq!(c.control_id, "SC-7");
}
// ── closed-set algebra contracts (ALL × as_str × FromStr × gates_phase) ──
/// Structural well-formedness of [`VerificationPhase`] as a
/// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
/// testkit lift that pins all three structural invariants
/// (`ALL` is non-empty, every variant round-trips through
/// `label ↔ parse_label`, labels are pairwise distinct, `""` is
/// outside the closed set) at ONE call site. Replaces the
/// hand-derived `verification_phase_all_is_unique_and_complete` +
/// `verification_phase_roundtrip_via_as_str` + the empty-input
/// arm of the per-implementor unknown-error test. `FromStr`
/// delegates to `<Self as tatara_lisp::ClosedSet>::parse_label`,
/// so this helper exercises the same code path the reconciler
/// hits when parsing a CRD `enum:`-validated value back to the
/// typed phase.
#[test]
fn verification_phase_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<VerificationPhase>();
}
/// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
/// output verbatim for every variant. A future variant rename
/// (or an `as_str` arm typo) lands here at one site, instead of
/// drifting between the typed surface and the YAML wire format
/// the reconciler / operator both read. NOT lifted into the
/// `ClosedSet` testkit — `serde_json` is NOT a `tatara-lisp`
/// dependency, and per-implementor serde-shape choices (PascalCase
/// for CRD enums, snake_case for camelCase carriers, lowercase
/// for Lisp keyword projections) make a generic helper a
/// category error.
#[test]
fn verification_phase_as_str_matches_serde() {
for phase in VerificationPhase::ALL {
let serialized = serde_json::to_string(&phase).expect("serialize");
let unquoted = serialized
.trim_start_matches('"')
.trim_end_matches('"')
.to_string();
assert_eq!(
unquoted,
phase.as_str(),
"as_str drift for {phase:?}: as_str={} serde={unquoted}",
phase.as_str()
);
}
}
/// The Display impl IS `as_str` — pinning this lets future callers
/// reach for either projection without drift.
#[test]
fn verification_phase_display_matches_as_str() {
for phase in VerificationPhase::ALL {
assert_eq!(phase.to_string(), phase.as_str());
}
}
/// `FromStr` rejects domain-specific non-canonical inputs and
/// the error echoes the input VERBATIM so the operator-facing
/// diagnostic carries the offending value. Kept per-implementor
/// because the verbatim-payload contract is a property of the
/// per-enum `Unknown<X>(pub String)` newtype, not of the trait's
/// structural surface. (The empty-input arm is now lifted into
/// `verification_phase_is_well_formed_closed_set`; the
/// case-drifted / hyphenated / extinct-variant arms stay here as
/// they're representative non-canonical inputs the operator
/// might supply.)
#[test]
fn unknown_verification_phase_errors() {
use std::str::FromStr;
for bad in [
"plantime",
"ATBOUNDARY",
"Plan-Time",
"post_convergence",
"Continuous",
] {
let err = VerificationPhase::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
/// TRUTH-TABLE CONTRACT: `gates_phase` agrees with the documented
/// per-variant codomain (the phase whose entry a violated binding
/// blocks, or `None` for non-blocking continuous-audit phases).
#[test]
fn verification_phase_gates_phase_truth_table() {
assert_eq!(
VerificationPhase::PlanTime.gates_phase(),
Some(ProcessPhase::Execing)
);
assert_eq!(
VerificationPhase::AtBoundary.gates_phase(),
Some(ProcessPhase::Attested)
);
assert_eq!(VerificationPhase::PostConvergence.gates_phase(), None);
}
/// SUBSET CONTRACT: every `Some(target)` `gates_phase` projects to
/// is a phase reachable as the destination of some legal
/// `ProcessPhase::can_transition_to` edge. A future variant that
/// projected to a `ProcessPhase` no transition leads into would
/// FAIL here, forcing the author to either pick a real gate phase
/// or extend `can_transition_to` deliberately. The reachability
/// check is the cross-enum coherence proof — the typed-phase
/// state machine and the verification-phase gate algebra agree on
/// which phases are gateable.
#[test]
fn verification_phase_gates_phase_projects_to_reachable_phases() {
for vp in VerificationPhase::ALL {
if let Some(target) = vp.gates_phase() {
let reachable = ProcessPhase::ALL
.into_iter()
.any(|src| src != target && src.can_transition_to(target));
assert!(
reachable,
"{vp:?}.gates_phase() = Some({target:?}) but no legal transition lands on {target:?}",
);
}
}
}
/// INJECTIVITY CONTRACT: distinct `Some` variants of `gates_phase`
/// project to distinct `ProcessPhase`s. Pairing this with the
/// subset contract above forces a future variant to land on a
/// fresh gateable phase (or project to `None` and be a deliberate
/// non-blocking auditor).
#[test]
fn verification_phase_gates_phase_is_injective() {
let projections: Vec<ProcessPhase> = VerificationPhase::ALL
.into_iter()
.filter_map(VerificationPhase::gates_phase)
.collect();
let unique: std::collections::HashSet<_> = projections.iter().copied().collect();
assert_eq!(
projections.len(),
unique.len(),
"gates_phase projection is not injective: {projections:?}",
);
}
}