Skip to main content

mongreldb_core/
certification.rs

1//! Machine-generated release certification manifests.
2//!
3//! A manifest is valid only when every recorded command passed, its evidence
4//! artifact exists, and its source and artifact identities match what the
5//! verifier expected. CI creates the manifest after executing the commands.
6//!
7//! Architecture task IDs follow the Stage 0–5 architecture specification
8//! (FND-*, S1*, S2*, S3*, S4*, S5*). Residual R1–R10 IDs are optional aliases
9//! for backward compatibility and never substitute for Stage IDs.
10//!
11//! Status rule (audit §2.4 / P0.9): Integrated ≠ Qualified. Validation accepts
12//! Integrated rows with traceability evidence. Qualified requires non-empty
13//! evidence that maps to recorded tests and at least one evidence class.
14
15use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, BTreeSet};
17
18/// Mandatory Stage 0–5 architecture task IDs.
19///
20/// Must stay aligned with
21/// `scripts/generate-certification-manifest.py::MANDATORY_ARCHITECTURE_TASKS`.
22pub const MANDATORY_ARCHITECTURE_TASK_IDS: &[&str] = &[
23    // Stage 0
24    "FND-001", "FND-002", "FND-003", "FND-004", "FND-005", "FND-006", "FND-007",
25    // Stage 1
26    "S1A-001", "S1A-002", "S1A-003", "S1A-004", "S1B-001", "S1B-002", "S1B-003", "S1B-004",
27    "S1B-005", "S1C-001", "S1C-002", "S1C-003", "S1C-004", "S1D-001", "S1D-002", "S1D-003",
28    "S1D-004", "S1D-005", "S1D-006", "S1D-007", "S1E-001", "S1E-002", "S1E-003", "S1E-004",
29    "S1F-001", "S1F-002", "S1F-003", "S1G", // Stage 2
30    "S2A-001", "S2A-002", "S2B-001", "S2B-002", "S2B-003", "S2B-004", "S2C", "S2D", "S2E", "S2F",
31    "S2G", "S2H", // Stage 3
32    "S3A", "S3B", "S3C", "S3D", "S3E", "S3F", "S3G", "S3H", "S3I", "S3J", "S3K", "S3L",
33    // Stage 4
34    "S4A", "S4B", "S4C", "S4D", "S4E", "S4F", "S4G", // Stage 5
35    "S5A", "S5B", "S5C", "S5D", "S5E", "S5F",
36];
37
38/// Optional residual R1–R10 aliases from the previous audit matrix.
39pub const RESIDUAL_ALIAS_TASK_IDS: &[&str] =
40    &["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10"];
41
42/// Evidence classes that may justify a Qualified promotion (audit §12 / P0.9-T3).
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum EvidenceClass {
46    SourceIntegration,
47    MultiProcess,
48    FaultChaos,
49    PackagedArtifact,
50    PerformanceSlo,
51    Security,
52    UpgradeCompatibility,
53    Documentation,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct CertificationManifest {
58    pub commit: String,
59    pub artifact_sha256: String,
60    pub implementation_status_sha256: String,
61    pub rust_version: String,
62    pub architecture_tasks: Vec<ArchitectureQualification>,
63    pub tests: Vec<CertificationTest>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct ArchitectureQualification {
68    pub id: String,
69    pub status: ArchitectureStatus,
70    pub evidence: Vec<String>,
71    /// Required and non-empty only when `status == Qualified`.
72    #[serde(default)]
73    pub evidence_classes: Vec<EvidenceClass>,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum ArchitectureStatus {
79    /// Production path exists; not release-Qualified without multi-class evidence.
80    Integrated,
81    /// Exact-SHA product evidence of the required classes has been recorded.
82    Qualified,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct CertificationTest {
87    pub id: String,
88    pub command: String,
89    pub status: CertificationStatus,
90    pub duration_ms: u64,
91    pub artifact: String,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum CertificationStatus {
97    Passed,
98    Failed,
99}
100
101/// Stage groupings for Stage-level qualification checks (P0.9-T2).
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum ArchitectureStage {
104    Stage0Foundations,
105    Stage1SingleNode,
106    Stage2ReplicatedHa,
107    Stage3Sharded,
108    Stage4AiNative,
109    Stage5AdaptersOps,
110}
111
112impl ArchitectureStage {
113    pub fn all() -> &'static [ArchitectureStage] {
114        &[
115            Self::Stage0Foundations,
116            Self::Stage1SingleNode,
117            Self::Stage2ReplicatedHa,
118            Self::Stage3Sharded,
119            Self::Stage4AiNative,
120            Self::Stage5AdaptersOps,
121        ]
122    }
123
124    pub fn label(self) -> &'static str {
125        match self {
126            Self::Stage0Foundations => "Stage 0",
127            Self::Stage1SingleNode => "Stage 1",
128            Self::Stage2ReplicatedHa => "Stage 2",
129            Self::Stage3Sharded => "Stage 3",
130            Self::Stage4AiNative => "Stage 4",
131            Self::Stage5AdaptersOps => "Stage 5",
132        }
133    }
134
135    pub fn task_ids(self) -> &'static [&'static str] {
136        match self {
137            Self::Stage0Foundations => &MANDATORY_ARCHITECTURE_TASK_IDS[0..7],
138            Self::Stage1SingleNode => &MANDATORY_ARCHITECTURE_TASK_IDS[7..35],
139            Self::Stage2ReplicatedHa => &MANDATORY_ARCHITECTURE_TASK_IDS[35..47],
140            Self::Stage3Sharded => &MANDATORY_ARCHITECTURE_TASK_IDS[47..59],
141            Self::Stage4AiNative => &MANDATORY_ARCHITECTURE_TASK_IDS[59..66],
142            Self::Stage5AdaptersOps => &MANDATORY_ARCHITECTURE_TASK_IDS[66..72],
143        }
144    }
145}
146
147fn is_residual_alias(id: &str) -> bool {
148    RESIDUAL_ALIAS_TASK_IDS.contains(&id)
149}
150
151fn mandatory_task_set() -> BTreeSet<&'static str> {
152    MANDATORY_ARCHITECTURE_TASK_IDS.iter().copied().collect()
153}
154
155impl CertificationManifest {
156    pub fn validate(
157        &self,
158        expected_commit: &str,
159        expected_artifact_sha256: &str,
160    ) -> Result<(), String> {
161        if self.commit != expected_commit {
162            return Err(format!(
163                "certification commit mismatch: expected {expected_commit}, got {}",
164                self.commit
165            ));
166        }
167        if self.artifact_sha256 != expected_artifact_sha256 {
168            return Err("certification artifact SHA-256 mismatch".into());
169        }
170        if self.implementation_status_sha256.len() != 64
171            || !self
172                .implementation_status_sha256
173                .bytes()
174                .all(|byte| byte.is_ascii_hexdigit())
175        {
176            return Err("certification implementation-status SHA-256 is invalid".into());
177        }
178        if self.tests.is_empty() {
179            return Err("certification manifest has no tests".into());
180        }
181        for test in &self.tests {
182            if test.id.is_empty() || test.command.is_empty() || test.artifact.is_empty() {
183                return Err("certification test has an empty required field".into());
184            }
185            if test.duration_ms == 0 {
186                return Err(format!(
187                    "certification test {} has no measured duration",
188                    test.id
189                ));
190            }
191            if test.status != CertificationStatus::Passed {
192                return Err(format!("certification test {} did not pass", test.id));
193            }
194        }
195
196        let test_ids = self
197            .tests
198            .iter()
199            .map(|test| test.id.as_str())
200            .collect::<BTreeSet<_>>();
201        if test_ids.len() != self.tests.len() {
202            return Err("certification tests must have unique ids".into());
203        }
204
205        let mandatory = mandatory_task_set();
206        let mut seen: BTreeSet<String> = BTreeSet::new();
207        let mut present_mandatory: BTreeSet<String> = BTreeSet::new();
208
209        for task in &self.architecture_tasks {
210            if task.id.is_empty() {
211                return Err("certification architecture task has an empty id".into());
212            }
213            if !seen.insert(task.id.clone()) {
214                return Err(format!(
215                    "certification architecture task {} is duplicated",
216                    task.id
217                ));
218            }
219            let is_mandatory = mandatory.contains(task.id.as_str());
220            let is_residual = is_residual_alias(&task.id);
221            if !is_mandatory && !is_residual {
222                return Err(format!(
223                    "certification architecture task {} is not a Stage 0–5 id or residual R1–R10 alias",
224                    task.id
225                ));
226            }
227            if is_mandatory {
228                present_mandatory.insert(task.id.clone());
229            }
230
231            match task.status {
232                ArchitectureStatus::Integrated => {
233                    // Traceability evidence is optional for Integrated, but when
234                    // present every entry must name a recorded test.
235                    if task
236                        .evidence
237                        .iter()
238                        .any(|evidence| !test_ids.contains(evidence.as_str()))
239                    {
240                        return Err(format!(
241                            "certification architecture task {} has invalid evidence",
242                            task.id
243                        ));
244                    }
245                    // Integrated must never claim product evidence classes.
246                    if !task.evidence_classes.is_empty() {
247                        return Err(format!(
248                            "certification architecture task {} is Integrated but lists evidence_classes (would imply Qualified)",
249                            task.id
250                        ));
251                    }
252                }
253                ArchitectureStatus::Qualified => {
254                    if task.evidence.is_empty() {
255                        return Err(format!(
256                            "certification architecture task {} is Qualified without evidence",
257                            task.id
258                        ));
259                    }
260                    if task
261                        .evidence
262                        .iter()
263                        .any(|evidence| !test_ids.contains(evidence.as_str()))
264                    {
265                        return Err(format!(
266                            "certification architecture task {} has invalid evidence",
267                            task.id
268                        ));
269                    }
270                    if task.evidence_classes.is_empty() {
271                        return Err(format!(
272                            "certification architecture task {} is Qualified without evidence_classes",
273                            task.id
274                        ));
275                    }
276                }
277            }
278        }
279
280        if present_mandatory.len() != mandatory.len() {
281            let missing = mandatory
282                .iter()
283                .filter(|id| !present_mandatory.contains(**id))
284                .copied()
285                .collect::<Vec<_>>();
286            return Err(format!(
287                "certification architecture tasks missing mandatory Stage 0–5 ids: {}",
288                missing.join(", ")
289            ));
290        }
291
292        Ok(())
293    }
294
295    /// Stage is Qualified only when every mandatory task in that stage is Qualified.
296    pub fn stage_is_qualified(&self, stage: ArchitectureStage) -> bool {
297        let by_id: BTreeMap<&str, &ArchitectureQualification> = self
298            .architecture_tasks
299            .iter()
300            .map(|task| (task.id.as_str(), task))
301            .collect();
302        stage.task_ids().iter().all(|id| {
303            by_id
304                .get(id)
305                .is_some_and(|task| task.status == ArchitectureStatus::Qualified)
306        })
307    }
308
309    /// True when every mandatory Stage 0–5 task is Qualified for this manifest.
310    pub fn architecture_is_qualified(&self) -> bool {
311        ArchitectureStage::all()
312            .iter()
313            .all(|stage| self.stage_is_qualified(*stage))
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    fn passed_tests() -> Vec<CertificationTest> {
322        vec![
323            CertificationTest {
324                id: "workspace_tests".into(),
325                command: "cargo test --workspace".into(),
326                status: CertificationStatus::Passed,
327                duration_ms: 1,
328                artifact: "cargo-test.log".into(),
329            },
330            CertificationTest {
331                id: "crash_matrix".into(),
332                command: "cargo test -p mongreldb-core --test crash".into(),
333                status: CertificationStatus::Passed,
334                duration_ms: 2,
335                artifact: "crash-matrix.log".into(),
336            },
337        ]
338    }
339
340    fn stage_manifest(status: ArchitectureStatus) -> CertificationManifest {
341        let evidence = match status {
342            ArchitectureStatus::Integrated => vec!["workspace_tests".into()],
343            ArchitectureStatus::Qualified => vec!["workspace_tests".into()],
344        };
345        let evidence_classes = match status {
346            ArchitectureStatus::Integrated => Vec::new(),
347            ArchitectureStatus::Qualified => vec![EvidenceClass::SourceIntegration],
348        };
349        CertificationManifest {
350            commit: "abc".into(),
351            artifact_sha256: "def".into(),
352            implementation_status_sha256: "0".repeat(64),
353            rust_version: "rustc test".into(),
354            architecture_tasks: MANDATORY_ARCHITECTURE_TASK_IDS
355                .iter()
356                .map(|id| ArchitectureQualification {
357                    id: (*id).to_owned(),
358                    status,
359                    evidence: evidence.clone(),
360                    evidence_classes: evidence_classes.clone(),
361                })
362                .collect(),
363            tests: passed_tests(),
364        }
365    }
366
367    #[test]
368    fn mandatory_task_list_matches_stage_slices() {
369        assert_eq!(MANDATORY_ARCHITECTURE_TASK_IDS.len(), 72);
370        let mut covered = 0usize;
371        for stage in ArchitectureStage::all() {
372            covered += stage.task_ids().len();
373        }
374        assert_eq!(covered, MANDATORY_ARCHITECTURE_TASK_IDS.len());
375        assert_eq!(
376            ArchitectureStage::Stage0Foundations.task_ids()[0],
377            "FND-001"
378        );
379        assert_eq!(
380            ArchitectureStage::Stage0Foundations.task_ids().last(),
381            Some(&"FND-007")
382        );
383        assert_eq!(ArchitectureStage::Stage1SingleNode.task_ids()[0], "S1A-001");
384        assert_eq!(
385            ArchitectureStage::Stage1SingleNode.task_ids().last(),
386            Some(&"S1G")
387        );
388        assert_eq!(
389            ArchitectureStage::Stage5AdaptersOps.task_ids().last(),
390            Some(&"S5F")
391        );
392    }
393
394    #[test]
395    fn validation_accepts_integrated_stage_matrix() {
396        let manifest = stage_manifest(ArchitectureStatus::Integrated);
397        assert!(manifest.validate("abc", "def").is_ok());
398        assert!(!manifest.architecture_is_qualified());
399        assert!(!manifest.stage_is_qualified(ArchitectureStage::Stage0Foundations));
400    }
401
402    #[test]
403    fn validation_accepts_optional_residual_aliases() {
404        let mut manifest = stage_manifest(ArchitectureStatus::Integrated);
405        for id in RESIDUAL_ALIAS_TASK_IDS {
406            manifest.architecture_tasks.push(ArchitectureQualification {
407                id: (*id).to_owned(),
408                status: ArchitectureStatus::Integrated,
409                evidence: vec!["workspace_tests".into()],
410                evidence_classes: Vec::new(),
411            });
412        }
413        assert!(manifest.validate("abc", "def").is_ok());
414    }
415
416    #[test]
417    fn validation_rejects_r1_r10_only_matrix() {
418        let manifest = CertificationManifest {
419            commit: "abc".into(),
420            artifact_sha256: "def".into(),
421            implementation_status_sha256: "0".repeat(64),
422            rust_version: "rustc test".into(),
423            architecture_tasks: (1..=10)
424                .map(|index| ArchitectureQualification {
425                    id: format!("R{index}"),
426                    status: ArchitectureStatus::Integrated,
427                    evidence: vec!["workspace_tests".into()],
428                    evidence_classes: Vec::new(),
429                })
430                .collect(),
431            tests: passed_tests(),
432        };
433        let err = manifest.validate("abc", "def").unwrap_err();
434        assert!(
435            err.contains("missing mandatory") || err.contains("FND-001"),
436            "unexpected error: {err}"
437        );
438    }
439
440    #[test]
441    fn validation_rejects_qualified_without_evidence_or_classes() {
442        let mut no_evidence = stage_manifest(ArchitectureStatus::Integrated);
443        no_evidence.architecture_tasks[0].status = ArchitectureStatus::Qualified;
444        no_evidence.architecture_tasks[0].evidence.clear();
445        no_evidence.architecture_tasks[0].evidence_classes = vec![EvidenceClass::SourceIntegration];
446        assert!(no_evidence
447            .validate("abc", "def")
448            .unwrap_err()
449            .contains("without evidence"));
450
451        let mut no_classes = stage_manifest(ArchitectureStatus::Integrated);
452        no_classes.architecture_tasks[0].status = ArchitectureStatus::Qualified;
453        no_classes.architecture_tasks[0].evidence = vec!["workspace_tests".into()];
454        no_classes.architecture_tasks[0].evidence_classes.clear();
455        assert!(no_classes
456            .validate("abc", "def")
457            .unwrap_err()
458            .contains("without evidence_classes"));
459    }
460
461    #[test]
462    fn validation_rejects_integrated_with_evidence_classes() {
463        let mut manifest = stage_manifest(ArchitectureStatus::Integrated);
464        manifest.architecture_tasks[0].evidence_classes = vec![EvidenceClass::SourceIntegration];
465        assert!(manifest
466            .validate("abc", "def")
467            .unwrap_err()
468            .contains("Integrated but lists evidence_classes"));
469    }
470
471    #[test]
472    fn stage_is_qualified_requires_every_task_in_stage() {
473        let mut manifest = stage_manifest(ArchitectureStatus::Integrated);
474        for task in &mut manifest.architecture_tasks {
475            if task.id.starts_with("FND-") {
476                task.status = ArchitectureStatus::Qualified;
477                task.evidence = vec!["workspace_tests".into()];
478                task.evidence_classes = vec![EvidenceClass::SourceIntegration];
479            }
480        }
481        assert!(manifest.validate("abc", "def").is_ok());
482        assert!(manifest.stage_is_qualified(ArchitectureStage::Stage0Foundations));
483        assert!(!manifest.stage_is_qualified(ArchitectureStage::Stage1SingleNode));
484        assert!(!manifest.architecture_is_qualified());
485
486        // Drop one Stage 0 task from Qualified → stage fails.
487        manifest.architecture_tasks[0].status = ArchitectureStatus::Integrated;
488        manifest.architecture_tasks[0].evidence_classes.clear();
489        assert!(!manifest.stage_is_qualified(ArchitectureStage::Stage0Foundations));
490    }
491
492    #[test]
493    fn validation_rejects_failed_or_wrong_identity_evidence() {
494        assert!(stage_manifest(ArchitectureStatus::Integrated)
495            .validate("abc", "def")
496            .is_ok());
497        let mut failed = stage_manifest(ArchitectureStatus::Integrated);
498        failed.tests[0].status = CertificationStatus::Failed;
499        assert!(failed.validate("abc", "def").is_err());
500        assert!(stage_manifest(ArchitectureStatus::Integrated)
501            .validate("other", "def")
502            .is_err());
503        assert!(stage_manifest(ArchitectureStatus::Integrated)
504            .validate("abc", "other")
505            .is_err());
506        let mut invalid_status = stage_manifest(ArchitectureStatus::Integrated);
507        invalid_status.implementation_status_sha256 = "not-a-hash".into();
508        assert!(invalid_status.validate("abc", "def").is_err());
509        let mut unmeasured = stage_manifest(ArchitectureStatus::Integrated);
510        unmeasured.tests[0].duration_ms = 0;
511        assert!(unmeasured.validate("abc", "def").is_err());
512        let mut missing_task = stage_manifest(ArchitectureStatus::Integrated);
513        missing_task.architecture_tasks.pop();
514        assert!(missing_task.validate("abc", "def").is_err());
515        let mut unknown_evidence = stage_manifest(ArchitectureStatus::Integrated);
516        unknown_evidence.architecture_tasks[0].evidence = vec!["missing".into()];
517        assert!(unknown_evidence.validate("abc", "def").is_err());
518        let mut unknown_task = stage_manifest(ArchitectureStatus::Integrated);
519        unknown_task
520            .architecture_tasks
521            .push(ArchitectureQualification {
522                id: "NOT-A-STAGE".into(),
523                status: ArchitectureStatus::Integrated,
524                evidence: Vec::new(),
525                evidence_classes: Vec::new(),
526            });
527        assert!(unknown_task
528            .validate("abc", "def")
529            .unwrap_err()
530            .contains("not a Stage"));
531    }
532
533    #[test]
534    fn fully_qualified_matrix_requires_evidence_classes() {
535        let manifest = stage_manifest(ArchitectureStatus::Qualified);
536        assert!(manifest.validate("abc", "def").is_ok());
537        assert!(manifest.architecture_is_qualified());
538        for stage in ArchitectureStage::all() {
539            assert!(manifest.stage_is_qualified(*stage), "{:?}", stage);
540        }
541    }
542}