1use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, BTreeSet};
17
18pub const MANDATORY_ARCHITECTURE_TASK_IDS: &[&str] = &[
23 "FND-001", "FND-002", "FND-003", "FND-004", "FND-005", "FND-006", "FND-007",
25 "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", "S2A-001", "S2A-002", "S2B-001", "S2B-002", "S2B-003", "S2B-004", "S2C", "S2D", "S2E", "S2F",
31 "S2G", "S2H", "S3A", "S3B", "S3C", "S3D", "S3E", "S3F", "S3G", "S3H", "S3I", "S3J", "S3K", "S3L",
33 "S4A", "S4B", "S4C", "S4D", "S4E", "S4F", "S4G", "S5A", "S5B", "S5C", "S5D", "S5E", "S5F",
36];
37
38pub const RESIDUAL_ALIAS_TASK_IDS: &[&str] =
40 &["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10"];
41
42#[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 #[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 Integrated,
81 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#[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 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 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 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 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 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}