1use serde::{Deserialize, Serialize};
40use std::collections::BTreeMap;
41
42pub const ARCHITECTURE_RECEIPT_SCHEMA: &str = "affidavit.architecture-qualification.v2";
44
45pub const ARCHITECTURE_QUERY_SCHEMA: &str = "affidavit.architecture-standing-query.v1";
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
50#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
51pub enum ArchitectureStanding {
52 Unknown,
54 Candidate,
56 Qualified,
58 Refused,
60 Superseded,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
67#[serde(rename_all = "kebab-case")]
68pub enum EvidenceSource {
69 AutofdeLab,
71 Xaas,
73 Runtime,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct QualificationEvidence {
84 pub source: EvidenceSource,
86 pub producer_digest: String,
88 pub content_digest: String,
90}
91
92impl QualificationEvidence {
93 pub fn observe(
96 source: EvidenceSource,
97 producer_digest: impl Into<String>,
98 bytes: &[u8],
99 ) -> Result<Self, ArchitectureRefusal> {
100 let producer_digest = producer_digest.into();
101 validate_digest("evidence.producer_digest", &producer_digest)?;
102 if bytes.is_empty() {
103 return Err(ArchitectureRefusal::MissingEvidence);
104 }
105 Ok(Self {
106 source,
107 producer_digest,
108 content_digest: blake3_digest(bytes),
109 })
110 }
111
112 pub fn binding_digest(&self) -> String {
114 let bytes = serde_json::to_vec(self).expect("evidence is serializable");
115 blake3_digest(&bytes)
116 }
117
118 pub fn verify_bytes(&self, bytes: &[u8]) -> Result<(), ArchitectureRefusal> {
120 validate_digest("evidence.producer_digest", &self.producer_digest)?;
121 if bytes.is_empty() || self.content_digest != blake3_digest(bytes) {
122 return Err(ArchitectureRefusal::ForgedEvidence);
123 }
124 Ok(())
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(deny_unknown_fields)]
131pub struct ArchitectureQualificationReceipt {
132 pub schema: String,
134 pub abb_digest: String,
136 pub contract_digest: String,
138 pub sbb_digest: String,
140 pub exact_subject_digest: String,
142 pub qualification_evidence_digests: Vec<String>,
144 pub producer_digest: String,
146 pub artifact_digests: Vec<String>,
148 pub standing: ArchitectureStanding,
150 pub confers_do_authority: bool,
152 pub prior_receipt_digest: Option<String>,
155 pub superseded_by_receipt_digest: Option<String>,
157 pub receipt_digest: String,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct Supersession {
165 pub retired: ArchitectureQualificationReceipt,
167 pub successor: ArchitectureQualificationReceipt,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum ArchitectureRefusal {
174 MissingExactSubject,
176 MissingEvidence,
178 UnknownPromotion,
180 MutableOrChangedContract,
182 MutableOrChangedSbb,
184 CrossSubjectReuse,
186 DoAuthorityForbidden,
188 ReplayMismatch,
190 MalformedDigest {
192 field: &'static str,
194 },
195 SchemaMismatch,
197 NotAReplacement,
199 ChainBroken,
201 StandingChainMismatch,
204 NotSupersedable,
206 AlreadySuperseded,
208 OrphanChainLink,
210 ForgedEvidence,
212 Malformed,
214}
215
216impl core::fmt::Display for ArchitectureRefusal {
217 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218 match self {
219 Self::MalformedDigest { field } => write!(f, "MALFORMED_DIGEST[{field}]"),
220 other => write!(f, "{other:?}"),
221 }
222 }
223}
224
225impl std::error::Error for ArchitectureRefusal {}
226
227fn blake3_digest(bytes: &[u8]) -> String {
228 format!("blake3:{}", blake3::hash(bytes).to_hex())
229}
230
231fn split_digest(value: &str) -> Option<(&str, &str)> {
232 let (algorithm, body) = value.split_once(':')?;
233 let algorithm_ok = !algorithm.is_empty()
234 && algorithm
235 .bytes()
236 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
237 let body_ok = !body.is_empty() && body.bytes().all(|b| b.is_ascii_graphic());
240 (algorithm_ok && body_ok).then_some((algorithm, body))
241}
242
243fn validate_digest(field: &'static str, value: &str) -> Result<(), ArchitectureRefusal> {
244 split_digest(value)
245 .map(|_| ())
246 .ok_or(ArchitectureRefusal::MalformedDigest { field })
247}
248
249fn validate_receipt_link(field: &'static str, value: &str) -> Result<(), ArchitectureRefusal> {
251 let ok = value.strip_prefix("blake3:").is_some_and(|hex| {
252 hex.len() == 64
253 && hex
254 .bytes()
255 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
256 });
257 if ok {
258 Ok(())
259 } else {
260 Err(ArchitectureRefusal::MalformedDigest { field })
261 }
262}
263
264fn canonical_set(
265 field: &'static str,
266 digests: Vec<String>,
267) -> Result<Vec<String>, ArchitectureRefusal> {
268 for digest in &digests {
269 validate_digest(field, digest)?;
270 }
271 let mut digests = digests;
272 digests.sort();
273 digests.dedup();
274 Ok(digests)
275}
276
277fn is_canonical(digests: &[String]) -> bool {
278 digests.windows(2).all(|pair| pair[0] < pair[1])
279}
280
281impl ArchitectureQualificationReceipt {
282 #[allow(clippy::too_many_arguments)]
288 pub fn certify(
289 abb_digest: impl Into<String>,
290 contract_digest: impl Into<String>,
291 sbb_digest: impl Into<String>,
292 exact_subject_digest: impl Into<String>,
293 qualification_evidence_digests: Vec<String>,
294 producer_digest: impl Into<String>,
295 artifact_digests: Vec<String>,
296 standing: ArchitectureStanding,
297 ) -> Result<Self, ArchitectureRefusal> {
298 let abb_digest = abb_digest.into();
299 let contract_digest = contract_digest.into();
300 let sbb_digest = sbb_digest.into();
301 let exact_subject_digest = exact_subject_digest.into();
302 let producer_digest = producer_digest.into();
303
304 if exact_subject_digest.is_empty() {
305 return Err(ArchitectureRefusal::MissingExactSubject);
306 }
307 if qualification_evidence_digests.is_empty() {
308 return Err(ArchitectureRefusal::MissingEvidence);
309 }
310 match standing {
311 ArchitectureStanding::Unknown => return Err(ArchitectureRefusal::UnknownPromotion),
312 ArchitectureStanding::Superseded => {
313 return Err(ArchitectureRefusal::StandingChainMismatch)
314 }
315 _ => {}
316 }
317 validate_digest("abb_digest", &abb_digest)?;
318 validate_digest("contract_digest", &contract_digest)?;
319 validate_digest("sbb_digest", &sbb_digest)?;
320 validate_digest("exact_subject_digest", &exact_subject_digest)?;
321 validate_digest("producer_digest", &producer_digest)?;
322 let qualification_evidence_digests = canonical_set(
323 "qualification_evidence_digests",
324 qualification_evidence_digests,
325 )?;
326 let artifact_digests = canonical_set("artifact_digests", artifact_digests)?;
327
328 let mut receipt = Self {
329 schema: ARCHITECTURE_RECEIPT_SCHEMA.to_string(),
330 abb_digest,
331 contract_digest,
332 sbb_digest,
333 exact_subject_digest,
334 qualification_evidence_digests,
335 producer_digest,
336 artifact_digests,
337 standing,
338 confers_do_authority: false,
339 prior_receipt_digest: None,
340 superseded_by_receipt_digest: None,
341 receipt_digest: String::new(),
342 };
343 receipt.reseal();
344 Ok(receipt)
345 }
346
347 #[allow(clippy::too_many_arguments)]
350 pub fn certify_from_evidence(
351 abb_digest: impl Into<String>,
352 contract_digest: impl Into<String>,
353 sbb_digest: impl Into<String>,
354 exact_subject_digest: impl Into<String>,
355 evidence: &[QualificationEvidence],
356 producer_digest: impl Into<String>,
357 artifact_digests: Vec<String>,
358 standing: ArchitectureStanding,
359 ) -> Result<Self, ArchitectureRefusal> {
360 for item in evidence {
361 validate_digest("evidence.producer_digest", &item.producer_digest)?;
362 validate_receipt_link("evidence.content_digest", &item.content_digest)?;
363 }
364 Self::certify(
365 abb_digest,
366 contract_digest,
367 sbb_digest,
368 exact_subject_digest,
369 evidence
370 .iter()
371 .map(QualificationEvidence::binding_digest)
372 .collect(),
373 producer_digest,
374 artifact_digests,
375 standing,
376 )
377 }
378
379 fn reseal(&mut self) {
380 self.receipt_digest = digest_without_receipt(self);
381 }
382
383 pub fn verify_integrity(&self) -> Result<(), ArchitectureRefusal> {
387 if self.schema != ARCHITECTURE_RECEIPT_SCHEMA {
388 return Err(ArchitectureRefusal::SchemaMismatch);
389 }
390 if self.confers_do_authority {
391 return Err(ArchitectureRefusal::DoAuthorityForbidden);
392 }
393 if self.standing == ArchitectureStanding::Unknown {
394 return Err(ArchitectureRefusal::UnknownPromotion);
395 }
396 if self.exact_subject_digest.is_empty() {
397 return Err(ArchitectureRefusal::MissingExactSubject);
398 }
399 if self.qualification_evidence_digests.is_empty() {
400 return Err(ArchitectureRefusal::MissingEvidence);
401 }
402 validate_digest("abb_digest", &self.abb_digest)?;
403 validate_digest("contract_digest", &self.contract_digest)?;
404 validate_digest("sbb_digest", &self.sbb_digest)?;
405 validate_digest("exact_subject_digest", &self.exact_subject_digest)?;
406 validate_digest("producer_digest", &self.producer_digest)?;
407 for digest in &self.qualification_evidence_digests {
408 validate_digest("qualification_evidence_digests", digest)?;
409 }
410 for digest in &self.artifact_digests {
411 validate_digest("artifact_digests", digest)?;
412 }
413 if let Some(prior) = &self.prior_receipt_digest {
414 validate_receipt_link("prior_receipt_digest", prior)?;
415 }
416 if let Some(successor) = &self.superseded_by_receipt_digest {
417 validate_receipt_link("superseded_by_receipt_digest", successor)?;
418 }
419 let chain_ok = match self.standing {
420 ArchitectureStanding::Superseded => match (
421 &self.prior_receipt_digest,
422 &self.superseded_by_receipt_digest,
423 ) {
424 (Some(prior), Some(successor)) => prior != successor,
425 _ => false,
426 },
427 ArchitectureStanding::Qualified => self.superseded_by_receipt_digest.is_none(),
428 _ => self.prior_receipt_digest.is_none() && self.superseded_by_receipt_digest.is_none(),
429 };
430 if !chain_ok {
431 return Err(ArchitectureRefusal::StandingChainMismatch);
432 }
433 if !is_canonical(&self.qualification_evidence_digests)
434 || !is_canonical(&self.artifact_digests)
435 {
436 return Err(ArchitectureRefusal::ReplayMismatch);
437 }
438 if self.receipt_digest != digest_without_receipt(self) {
439 return Err(ArchitectureRefusal::ReplayMismatch);
440 }
441 Ok(())
442 }
443
444 pub fn verify_replay(
447 &self,
448 expected_abb_digest: &str,
449 expected_contract_digest: &str,
450 expected_sbb_digest: &str,
451 expected_subject_digest: &str,
452 ) -> Result<(), ArchitectureRefusal> {
453 self.verify_integrity()?;
454 if self.abb_digest != expected_abb_digest
455 || self.exact_subject_digest != expected_subject_digest
456 {
457 return Err(ArchitectureRefusal::CrossSubjectReuse);
458 }
459 if self.contract_digest != expected_contract_digest {
460 return Err(ArchitectureRefusal::MutableOrChangedContract);
461 }
462 if self.sbb_digest != expected_sbb_digest {
463 return Err(ArchitectureRefusal::MutableOrChangedSbb);
464 }
465 Ok(())
466 }
467
468 pub fn verify_evidence(
473 &self,
474 observed: &[(QualificationEvidence, &[u8])],
475 ) -> Result<(), ArchitectureRefusal> {
476 self.verify_integrity()?;
477 for (item, bytes) in observed {
478 item.verify_bytes(bytes)?;
479 }
480 let mut derived: Vec<String> = observed
481 .iter()
482 .map(|(item, _)| item.binding_digest())
483 .collect();
484 derived.sort();
485 derived.dedup();
486 if derived != self.qualification_evidence_digests {
487 return Err(ArchitectureRefusal::ForgedEvidence);
488 }
489 Ok(())
490 }
491
492 pub fn supersede(
497 &self,
498 new_sbb_digest: impl Into<String>,
499 new_subject_digest: impl Into<String>,
500 evidence: Vec<String>,
501 ) -> Result<Supersession, ArchitectureRefusal> {
502 self.verify_integrity()?;
503 if self.standing != ArchitectureStanding::Qualified {
504 return Err(ArchitectureRefusal::NotSupersedable);
505 }
506 let new_sbb_digest = new_sbb_digest.into();
507 if same_sbb(&new_sbb_digest, &self.sbb_digest) {
508 return Err(ArchitectureRefusal::NotAReplacement);
509 }
510 let mut successor = Self::certify(
511 self.abb_digest.clone(),
512 self.contract_digest.clone(),
513 new_sbb_digest,
514 new_subject_digest,
515 evidence,
516 self.producer_digest.clone(),
517 self.artifact_digests.clone(),
518 ArchitectureStanding::Qualified,
519 )?;
520 successor.prior_receipt_digest = Some(self.receipt_digest.clone());
521 successor.reseal();
522
523 let mut retired = self.clone();
524 retired.standing = ArchitectureStanding::Superseded;
525 retired.prior_receipt_digest = Some(self.receipt_digest.clone());
526 retired.superseded_by_receipt_digest = Some(successor.receipt_digest.clone());
527 retired.reseal();
528
529 Ok(Supersession { retired, successor })
530 }
531
532 pub fn verify_chain(&self, prior: &Self) -> Result<(), ArchitectureRefusal> {
535 prior.verify_integrity()?;
536 self.verify_integrity()?;
537 if self.prior_receipt_digest.as_deref() != Some(prior.receipt_digest.as_str())
538 || self.standing != ArchitectureStanding::Qualified
539 {
540 return Err(ArchitectureRefusal::ChainBroken);
541 }
542 if prior.standing != ArchitectureStanding::Qualified {
543 return Err(ArchitectureRefusal::NotSupersedable);
544 }
545 if self.abb_digest != prior.abb_digest {
546 return Err(ArchitectureRefusal::CrossSubjectReuse);
547 }
548 if self.contract_digest != prior.contract_digest {
549 return Err(ArchitectureRefusal::MutableOrChangedContract);
550 }
551 if same_sbb(&self.sbb_digest, &prior.sbb_digest) {
552 return Err(ArchitectureRefusal::NotAReplacement);
553 }
554 Ok(())
555 }
556
557 pub fn to_json(&self) -> String {
559 serde_json::to_string(self).expect("architecture receipt is serializable")
560 }
561
562 pub fn from_json_verified(json: &str) -> Result<Self, ArchitectureRefusal> {
565 let receipt: Self =
566 serde_json::from_str(json).map_err(|_| ArchitectureRefusal::Malformed)?;
567 receipt.verify_integrity()?;
568 Ok(receipt)
569 }
570}
571
572impl Supersession {
573 pub fn verify(
577 &self,
578 prior: &ArchitectureQualificationReceipt,
579 ) -> Result<(), ArchitectureRefusal> {
580 self.successor.verify_chain(prior)?;
581 self.retired.verify_integrity()?;
582 let r = &self.retired;
583 if r.standing != ArchitectureStanding::Superseded
584 || r.prior_receipt_digest.as_deref() != Some(prior.receipt_digest.as_str())
585 || r.superseded_by_receipt_digest.as_deref()
586 != Some(self.successor.receipt_digest.as_str())
587 {
588 return Err(ArchitectureRefusal::ChainBroken);
589 }
590 let same_content = r.abb_digest == prior.abb_digest
591 && r.contract_digest == prior.contract_digest
592 && r.sbb_digest == prior.sbb_digest
593 && r.exact_subject_digest == prior.exact_subject_digest
594 && r.qualification_evidence_digests == prior.qualification_evidence_digests
595 && r.producer_digest == prior.producer_digest
596 && r.artifact_digests == prior.artifact_digests;
597 if !same_content {
598 return Err(ArchitectureRefusal::ChainBroken);
599 }
600 Ok(())
601 }
602}
603
604fn same_sbb(a: &str, b: &str) -> bool {
605 if a == b {
606 return true;
607 }
608 match (split_digest(a), split_digest(b)) {
609 (Some((_, body_a)), Some((_, body_b))) => body_a == body_b,
610 _ => false,
611 }
612}
613
614fn digest_without_receipt(receipt: &ArchitectureQualificationReceipt) -> String {
615 let mut projection = receipt.clone();
616 projection.receipt_digest.clear();
617 let bytes = serde_json::to_vec(&projection).expect("architecture receipt is serializable");
618 blake3_digest(&bytes)
619}
620
621#[derive(Debug, Clone, Default)]
626pub struct ArchitectureStandingLedger {
627 receipts: BTreeMap<String, ArchitectureQualificationReceipt>,
628 retired_by: BTreeMap<String, String>,
630}
631
632impl ArchitectureStandingLedger {
633 pub fn new() -> Self {
635 Self::default()
636 }
637
638 pub fn len(&self) -> usize {
640 self.receipts.len()
641 }
642
643 pub fn is_empty(&self) -> bool {
645 self.receipts.is_empty()
646 }
647
648 pub fn admit(
654 &mut self,
655 receipt: ArchitectureQualificationReceipt,
656 ) -> Result<(), ArchitectureRefusal> {
657 receipt.verify_integrity()?;
658 if receipt.prior_receipt_digest.is_some() {
659 let prior = receipt.prior_receipt_digest.as_deref().unwrap_or_default();
660 return Err(if self.receipts.contains_key(prior) {
661 ArchitectureRefusal::ChainBroken
662 } else {
663 ArchitectureRefusal::OrphanChainLink
664 });
665 }
666 self.receipts
667 .insert(receipt.receipt_digest.clone(), receipt);
668 Ok(())
669 }
670
671 pub fn admit_supersession(
675 &mut self,
676 supersession: Supersession,
677 ) -> Result<(), ArchitectureRefusal> {
678 let prior_digest = supersession
679 .successor
680 .prior_receipt_digest
681 .clone()
682 .ok_or(ArchitectureRefusal::ChainBroken)?;
683 let prior = self
684 .receipts
685 .get(&prior_digest)
686 .ok_or(ArchitectureRefusal::OrphanChainLink)?;
687 supersession.verify(prior)?;
688 if let Some(existing) = self.retired_by.get(&prior_digest) {
689 if existing == &supersession.successor.receipt_digest {
690 return Ok(());
691 }
692 return Err(ArchitectureRefusal::AlreadySuperseded);
693 }
694 self.retired_by
695 .insert(prior_digest, supersession.successor.receipt_digest.clone());
696 let Supersession { retired, successor } = supersession;
697 self.receipts
698 .insert(successor.receipt_digest.clone(), successor);
699 self.receipts
700 .insert(retired.receipt_digest.clone(), retired);
701 Ok(())
702 }
703
704 pub fn standing_of(&self, receipt_digest: &str) -> Option<ArchitectureStanding> {
707 let receipt = self.receipts.get(receipt_digest)?;
708 if self.retired_by.contains_key(receipt_digest) {
709 Some(ArchitectureStanding::Superseded)
710 } else {
711 Some(receipt.standing)
712 }
713 }
714
715 pub fn current_qualified(&self, abb_digest: &str) -> Vec<&ArchitectureQualificationReceipt> {
718 self.receipts
719 .values()
720 .filter(|r| {
721 r.abb_digest == abb_digest
722 && r.standing == ArchitectureStanding::Qualified
723 && !self.retired_by.contains_key(&r.receipt_digest)
724 })
725 .collect()
726 }
727
728 pub fn query_json(&self, abb_digest: &str) -> String {
731 let current: Vec<serde_json::Value> = self
732 .current_qualified(abb_digest)
733 .into_iter()
734 .map(|r| {
735 serde_json::json!({
736 "sbb_digest": r.sbb_digest,
737 "contract_digest": r.contract_digest,
738 "exact_subject_digest": r.exact_subject_digest,
739 "receipt_digest": r.receipt_digest,
740 "prior_receipt_digest": r.prior_receipt_digest,
741 })
742 })
743 .collect();
744 let superseded: Vec<serde_json::Value> = self
745 .receipts
746 .values()
747 .filter(|r| {
748 r.abb_digest == abb_digest && r.standing == ArchitectureStanding::Superseded
749 })
750 .map(|r| {
751 serde_json::json!({
752 "sbb_digest": r.sbb_digest,
753 "retired_receipt_digest": r.prior_receipt_digest,
754 "superseded_by_receipt_digest": r.superseded_by_receipt_digest,
755 "record_digest": r.receipt_digest,
756 })
757 })
758 .collect();
759 serde_json::json!({
760 "schema": ARCHITECTURE_QUERY_SCHEMA,
761 "abb_digest": abb_digest,
762 "confers_do_authority": false,
763 "current_qualified": current,
764 "superseded": superseded,
765 })
766 .to_string()
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773
774 fn receipt() -> ArchitectureQualificationReceipt {
775 ArchitectureQualificationReceipt::certify(
776 "sha256:abb",
777 "sha256:contract",
778 "sha256:sbb",
779 "sha256:subject",
780 vec!["sha256:evidence".into()],
781 "sha256:producer",
782 vec!["sha256:artifact".into()],
783 ArchitectureStanding::Qualified,
784 )
785 .unwrap()
786 }
787
788 #[test]
789 fn qualification_receipt_is_deterministic_and_non_authoritative() {
790 let first = receipt();
791 let second = receipt();
792 assert_eq!(first, second);
793 assert!(!first.confers_do_authority);
794 first
795 .verify_replay(
796 "sha256:abb",
797 "sha256:contract",
798 "sha256:sbb",
799 "sha256:subject",
800 )
801 .unwrap();
802 }
803
804 #[test]
805 fn changed_contract_sbb_and_cross_subject_reuse_are_refused() {
806 let receipt = receipt();
807
808 assert_eq!(
809 receipt.verify_replay(
810 "sha256:abb",
811 "sha256:changed",
812 "sha256:sbb",
813 "sha256:subject"
814 ),
815 Err(ArchitectureRefusal::MutableOrChangedContract)
816 );
817 assert_eq!(
818 receipt.verify_replay(
819 "sha256:abb",
820 "sha256:contract",
821 "sha256:changed",
822 "sha256:subject"
823 ),
824 Err(ArchitectureRefusal::MutableOrChangedSbb)
825 );
826 assert_eq!(
827 receipt.verify_replay(
828 "sha256:other",
829 "sha256:contract",
830 "sha256:sbb",
831 "sha256:other-subject"
832 ),
833 Err(ArchitectureRefusal::CrossSubjectReuse)
834 );
835 }
836
837 #[test]
838 fn unknown_and_missing_evidence_cannot_acquire_standing() {
839 assert_eq!(
840 ArchitectureQualificationReceipt::certify(
841 "sha256:abb",
842 "sha256:contract",
843 "sha256:sbb",
844 "sha256:subject",
845 vec!["sha256:evidence".into()],
846 "sha256:producer",
847 vec![],
848 ArchitectureStanding::Unknown
849 ),
850 Err(ArchitectureRefusal::UnknownPromotion)
851 );
852
853 assert_eq!(
854 ArchitectureQualificationReceipt::certify(
855 "sha256:abb",
856 "sha256:contract",
857 "sha256:sbb",
858 "sha256:subject",
859 vec![],
860 "sha256:producer",
861 vec![],
862 ArchitectureStanding::Qualified
863 ),
864 Err(ArchitectureRefusal::MissingEvidence)
865 );
866 }
867
868 #[test]
869 fn replacement_qualifies_successor_and_supersedes_the_prior() {
870 let current = receipt();
871 let s = current
872 .supersede(
873 "sha256:sbb-b",
874 "sha256:subject-b",
875 vec!["sha256:evidence-b".into()],
876 )
877 .unwrap();
878
879 assert_eq!(s.successor.standing, ArchitectureStanding::Qualified);
880 assert_eq!(s.successor.sbb_digest, "sha256:sbb-b");
881 assert_eq!(
882 s.successor.prior_receipt_digest.as_deref(),
883 Some(current.receipt_digest.as_str())
884 );
885 assert_eq!(s.retired.standing, ArchitectureStanding::Superseded);
886 assert_eq!(s.retired.sbb_digest, "sha256:sbb");
887 assert_eq!(
888 s.retired.superseded_by_receipt_digest.as_deref(),
889 Some(s.successor.receipt_digest.as_str())
890 );
891 assert!(!s.successor.confers_do_authority && !s.retired.confers_do_authority);
892 s.verify(¤t).unwrap();
893 }
894}