spec_driven_docs/plan/
evidence.rs1use serde::{Deserialize, Serialize};
10
11use crate::domain::ownership::Sha256;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum Producer {
17 Disk,
19 Record,
21 Declaration,
23 Bundle,
25 Registry,
27 Host,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct Evidence {
34 pub id: String,
36 pub about: String,
38 pub producer: Producer,
40 pub observed_at: String,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub sha256: Option<Sha256>,
45 pub method: String,
47}
48
49#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
51pub struct Ledger {
52 pub items: Vec<Evidence>,
54}
55
56impl Ledger {
57 #[must_use]
59 pub const fn new() -> Self {
60 Self { items: Vec::new() }
61 }
62
63 pub fn record(
65 &mut self,
66 id: &str,
67 about: &str,
68 producer: Producer,
69 observed_at: &str,
70 sha256: Option<Sha256>,
71 method: &str,
72 ) -> String {
73 self.items.push(Evidence {
74 id: id.to_string(),
75 about: about.to_string(),
76 producer,
77 observed_at: observed_at.to_string(),
78 sha256,
79 method: method.to_string(),
80 });
81 id.to_string()
82 }
83
84 #[must_use]
86 pub fn holds(&self, id: &str) -> bool {
87 self.items.iter().any(|item| item.id == id)
88 }
89
90 #[must_use]
92 pub fn ids(&self) -> Vec<&str> {
93 self.items.iter().map(|item| item.id.as_str()).collect()
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn a_recorded_observation_is_citable_by_the_reference_it_returns() {
103 let mut ledger = Ledger::new();
104 let reference = ledger.record(
105 "record",
106 "the instance manifest",
107 Producer::Record,
108 "2026-09-12T00:00:00Z",
109 Some(Sha256::of(b"x")),
110 "read from disk",
111 );
112 assert_eq!(reference, "record");
113 assert!(ledger.holds("record"));
114 assert!(!ledger.holds("absent"));
115 assert_eq!(ledger.ids(), ["record"]);
116 }
117
118 #[test]
119 fn an_observation_without_bytes_carries_no_digest() {
120 let mut ledger = Ledger::new();
121 ledger.record(
122 "host",
123 "the resolved paths",
124 Producer::Host,
125 "2026-09-12T00:00:00Z",
126 None,
127 "read from the environment",
128 );
129 assert_eq!(ledger.items[0].sha256, None);
130 assert_eq!(ledger.items[0].producer, Producer::Host);
131 }
132}